mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-07 20:25:10 +08:00
feat: add settings ui
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
max-width: 300px;
|
||||
width: 300px;
|
||||
padding: 20px;
|
||||
background-color: var(--second);
|
||||
display: flex;
|
||||
@@ -141,14 +141,19 @@
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.window-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
.window-header {
|
||||
padding: 14px 20px;
|
||||
border-bottom: rgba(0, 0, 0, 0.1) 1px solid;
|
||||
|
||||
@@ -157,7 +162,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-header-title {
|
||||
.window-header-title {
|
||||
font-size: 20px;
|
||||
font-weight: bolder;
|
||||
overflow: hidden;
|
||||
@@ -167,7 +172,7 @@
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.chat-header-sub-title {
|
||||
.window-header-sub-title {
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -303,3 +308,16 @@
|
||||
right: 30px;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
.settings {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-title {
|
||||
font-size: 14px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@@ -6,6 +6,8 @@ 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 { IconButton } from "./button";
|
||||
import styles from "./home.module.css";
|
||||
|
||||
@@ -20,7 +22,8 @@ import AddIcon from "../icons/add.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
|
||||
import { Message, useChatStore } from "../store";
|
||||
import { Message, SubmitKey, useChatStore } from "../store";
|
||||
import { Card, List, ListItem, Popover } from "./ui-lib";
|
||||
|
||||
export function Markdown(props: { content: string }) {
|
||||
return (
|
||||
@@ -31,11 +34,17 @@ export function Markdown(props: { content: string }) {
|
||||
}
|
||||
|
||||
export function Avatar(props: { role: Message["role"] }) {
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
if (props.role === "assistant") {
|
||||
return <BotIcon className={styles["user-avtar"]} />;
|
||||
}
|
||||
|
||||
return <div className={styles["user-avtar"]}>🤣</div>;
|
||||
return (
|
||||
<div className={styles["user-avtar"]}>
|
||||
<Emoji unified={config.avatar} size={18} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatItem(props: {
|
||||
@@ -148,11 +157,11 @@ export function Chat() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.topic}>
|
||||
<div className={styles["chat-header"]}>
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<div className={styles["window-header"]}>
|
||||
<div>
|
||||
<div className={styles["chat-header-title"]}>{session.topic}</div>
|
||||
<div className={styles["chat-header-sub-title"]}>
|
||||
<div className={styles["window-header-title"]}>{session.topic}</div>
|
||||
<div className={styles["window-header-sub-title"]}>
|
||||
与 ChatGPT 的 {session.messages.length} 条对话
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,7 +190,7 @@ export function Chat() {
|
||||
<div className={styles["chat-message-avatar"]}>
|
||||
<Avatar role={message.role} />
|
||||
</div>
|
||||
{message.preview && (
|
||||
{(message.preview || message.streaming) && (
|
||||
<div className={styles["chat-message-status"]}>正在输入…</div>
|
||||
)}
|
||||
<div className={styles["chat-message-item"]}>
|
||||
@@ -235,6 +244,9 @@ export function Chat() {
|
||||
export function Home() {
|
||||
const [createNewSession] = useChatStore((state) => [state.newSession]);
|
||||
|
||||
// settings
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.sidebar}>
|
||||
@@ -255,7 +267,10 @@ export function Home() {
|
||||
<div className={styles["sidebar-tail"]}>
|
||||
<div className={styles["sidebar-actions"]}>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<IconButton icon={<SettingsIcon />} />
|
||||
<IconButton
|
||||
icon={<SettingsIcon />}
|
||||
onClick={() => setOpenSettings(!openSettings)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["sidebar-action"]}>
|
||||
<a href="https://github.com/Yidadaa" target="_blank">
|
||||
@@ -273,7 +288,94 @@ export function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Chat key="chat" />
|
||||
<div className={styles["window-content"]}>
|
||||
{openSettings ? <Settings /> : <Chat key="chat" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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) => [
|
||||
state.config,
|
||||
state.updateConfig,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles["window-header"]}>
|
||||
<div>
|
||||
<div className={styles["window-header-title"]}>设置</div>
|
||||
<div className={styles["window-header-sub-title"]}>设置选项</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["settings"]}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<div className={styles["settings-title"]}>头像</div>
|
||||
<Popover
|
||||
onClose={() => setShowEmojiPicker(false)}
|
||||
content={
|
||||
<EmojiPicker
|
||||
lazyLoadEmojis
|
||||
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.entries(SubmitKey).map(([k, v]) => (
|
||||
<option value={k} key={v}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
<List>
|
||||
<ListItem>
|
||||
<div className={styles["settings-title"]}>最大记忆历史消息数</div>
|
||||
<div className="">{config.historyMessageCount}</div>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<div className={styles["settings-title"]}>发送机器人回复消息</div>
|
||||
<div className="">{config.sendBotMessages ? "是" : "否"}</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
56
app/components/ui-lib.module.css
Normal file
56
app/components/ui-lib.module.css
Normal file
@@ -0,0 +1,56 @@
|
||||
.card {
|
||||
background-color: var(--white);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--card-shadow);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.popover {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
position: absolute;
|
||||
animation: slide-in 0.3s ease;
|
||||
right: 0;
|
||||
top: calc(100% + 10px);
|
||||
}
|
||||
|
||||
.popover-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
border-bottom: var(--border-in-light);
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.list {
|
||||
border: var(--border-in-light);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--card-shadow);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.list .list-item:last-child {
|
||||
border: 0;
|
||||
}
|
38
app/components/ui-lib.tsx
Normal file
38
app/components/ui-lib.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import styles from "./ui-lib.module.css";
|
||||
|
||||
export function Popover(props: {
|
||||
children: JSX.Element;
|
||||
content: JSX.Element;
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.popover}>
|
||||
{props.children}
|
||||
{props.open && (
|
||||
<div className={styles["popover-content"]}>
|
||||
<div className={styles["popover-mask"]} onClick={props.onClose}></div>
|
||||
{props.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Card(props: { children: JSX.Element[]; className?: string }) {
|
||||
return (
|
||||
<div className={styles.card + " " + props.className}>{props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ListItem(props: { children: JSX.Element[] }) {
|
||||
if (props.children.length > 2) {
|
||||
throw Error("Only Support Two Children");
|
||||
}
|
||||
|
||||
return <div className={styles["list-item"]}>{props.children}</div>;
|
||||
}
|
||||
|
||||
export function List(props: { children: JSX.Element[] }) {
|
||||
return <div className={styles.list}>{props.children}</div>;
|
||||
}
|
Reference in New Issue
Block a user