diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss
index 7cd2889f..3a1be391 100644
--- a/app/components/chat.module.scss
+++ b/app/components/chat.module.scss
@@ -53,6 +53,20 @@
}
}
+.section-title {
+ font-size: 12px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .section-title-action {
+ display: flex;
+ align-items: center;
+ }
+}
+
.context-prompt {
.context-prompt-row {
display: flex;
@@ -81,25 +95,13 @@
}
.memory-prompt {
- margin-top: 20px;
-
- .memory-prompt-title {
- font-size: 12px;
- font-weight: bold;
- margin-bottom: 10px;
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- .memory-prompt-action {
- display: flex;
- align-items: center;
- }
- }
+ margin: 20px 0;
.memory-prompt-content {
- background-color: var(--gray);
- border-radius: 6px;
+ background-color: var(--white);
+ color: var(--black);
+ border: var(--border-in-light);
+ border-radius: 10px;
padding: 10px;
font-size: 12px;
user-select: text;
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index c5cc5429..867fbc49 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -1,4 +1,4 @@
-import { useDebounce, useDebouncedCallback } from "use-debounce";
+import { useDebouncedCallback } from "use-debounce";
import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
import SendWhiteIcon from "../icons/send-white.svg";
@@ -9,8 +9,6 @@ import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import LoadingIcon from "../icons/three-dots.svg";
-import BotIcon from "../icons/bot.svg";
-import BlackBotIcon from "../icons/black-bot.svg";
import AddIcon from "../icons/add.svg";
import DeleteIcon from "../icons/delete.svg";
import MaxIcon from "../icons/max.svg";
@@ -33,12 +31,13 @@ import {
Theme,
ModelType,
useAppConfig,
+ ModelConfig,
+ DEFAULT_TOPIC,
} from "../store";
import {
copyToClipboard,
downloadAs,
- getEmojiUrl,
selectOrCopy,
autoGrowTextArea,
useMobileScreen,
@@ -54,10 +53,11 @@ import { IconButton } from "./button";
import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss";
-import { Input, Modal, showModal } from "./ui-lib";
+import { Input, List, ListItem, Modal, Popover, showModal } from "./ui-lib";
import { useNavigate } from "react-router-dom";
import { Path } from "../constant";
-
+import { ModelConfigList } from "./model-config";
+import { AvatarPicker } from "./emoji";
const Markdown = dynamic(
async () => memo((await import("./markdown")).Markdown),
{
@@ -65,32 +65,10 @@ const Markdown = dynamic(
},
);
-const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
+const Avatar = dynamic(async () => (await import("./emoji")).Avatar, {
loading: () => ,
});
-export function Avatar(props: { role: Message["role"]; model?: ModelType }) {
- const config = useAppConfig();
-
- if (props.role !== "user") {
- return (
-
- {props.model?.startsWith("gpt-4") ? (
-
- ) : (
-
- )}
-
- );
- }
-
- return (
-
-
-
- );
-}
-
function exportMessages(messages: Message[], topic: string) {
const mdText =
`# ${topic}\n\n` +
@@ -129,15 +107,13 @@ function exportMessages(messages: Message[], topic: string) {
});
}
-function PromptToast(props: {
- showToast?: boolean;
- showModal?: boolean;
- setShowModal: (_: boolean) => void;
-}) {
+function ContextPrompts() {
const chatStore = useChatStore();
const session = chatStore.currentSession();
const context = session.context;
+ const [showPicker, setShowPicker] = useState(false);
+
const addContextPrompt = (prompt: Message) => {
chatStore.updateCurrentSession((session) => {
session.context.push(prompt);
@@ -156,6 +132,165 @@ function PromptToast(props: {
});
};
+ return (
+ <>
+
+ {context.map((c, i) => (
+
+
+
+ updateContextPrompt(i, {
+ ...c,
+ content: e.currentTarget.value as any,
+ })
+ }
+ />
+ }
+ className={chatStyle["context-delete-button"]}
+ onClick={() => removeContextPrompt(i)}
+ bordered
+ />
+
+ ))}
+
+
+ }
+ text={Locale.Context.Add}
+ bordered
+ className={chatStyle["context-prompt-button"]}
+ onClick={() =>
+ addContextPrompt({
+ role: "system",
+ content: "",
+ date: "",
+ })
+ }
+ />
+
+
+
+
+
+ chatStore.updateCurrentSession(
+ (session) => (session.avatar = emoji),
+ )
+ }
+ >
+ }
+ open={showPicker}
+ onClose={() => setShowPicker(false)}
+ >
+ setShowPicker(true)}>
+ {session.avatar ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ chatStore.updateCurrentSession(
+ (session) => (session.topic = e.currentTarget.value),
+ )
+ }
+ >
+
+
+
+ >
+ );
+}
+
+export function SessionConfigModel(props: { onClose: () => void }) {
+ const chatStore = useChatStore();
+ const config = useAppConfig();
+ const session = chatStore.currentSession();
+ const context = session.context;
+
+ const updateConfig = (updater: (config: ModelConfig) => void) => {
+ const config = { ...session.modelConfig };
+ updater(config);
+ chatStore.updateCurrentSession((session) => (session.modelConfig = config));
+ };
+
+ return (
+
+ props.onClose()}
+ actions={[
+ }
+ bordered
+ text="重置预设"
+ onClick={() =>
+ confirm(Locale.Memory.ResetConfirm) && chatStore.resetSession()
+ }
+ />,
+ }
+ bordered
+ text="保存预设"
+ onClick={() => copyToClipboard(session.memoryPrompt)}
+ />,
+ ]}
+ >
+
+
+
+
+
+ );
+}
+
+function PromptToast(props: {
+ showToast?: boolean;
+ showModal?: boolean;
+ setShowModal: (_: boolean) => void;
+}) {
+ const chatStore = useChatStore();
+ const session = chatStore.currentSession();
+ const context = session.context;
+
return (
{props.showToast && (
@@ -171,115 +306,7 @@ function PromptToast(props: {
)}
{props.showModal && (
-
-
props.setShowModal(false)}
- actions={[
- }
- bordered
- text={Locale.Memory.Reset}
- onClick={() =>
- confirm(Locale.Memory.ResetConfirm) &&
- chatStore.resetSession()
- }
- />,
- }
- bordered
- text={Locale.Memory.Copy}
- onClick={() => copyToClipboard(session.memoryPrompt)}
- />,
- ]}
- >
- <>
-
- {context.map((c, i) => (
-
-
-
- updateContextPrompt(i, {
- ...c,
- content: e.currentTarget.value as any,
- })
- }
- />
- }
- className={chatStyle["context-delete-button"]}
- onClick={() => removeContextPrompt(i)}
- bordered
- />
-
- ))}
-
-
- }
- text={Locale.Context.Add}
- bordered
- className={chatStyle["context-prompt-button"]}
- onClick={() =>
- addContextPrompt({
- role: "system",
- content: "",
- date: "",
- })
- }
- />
-
-
-
-
-
- {Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
- {session.messages.length})
-
-
-
-
-
- {session.memoryPrompt || Locale.Memory.EmptyContent}
-
-
- >
-
-
+ props.setShowModal(false)} />
)}
);
@@ -654,7 +681,7 @@ export function Chat() {
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
onClickCapture={renameSession}
>
- {session.topic}
+ {!session.topic ? DEFAULT_TOPIC : session.topic}
{Locale.Chat.SubTitle(session.messages.length)}
@@ -739,7 +766,13 @@ export function Chat() {
>
-
+ {message.role === "user" ? (
+
+ ) : session.avatar ? (
+
+ ) : (
+
+ )}
{showTyping && (
diff --git a/app/components/emoji.tsx b/app/components/emoji.tsx
new file mode 100644
index 00000000..b1d092a5
--- /dev/null
+++ b/app/components/emoji.tsx
@@ -0,0 +1,59 @@
+import EmojiPicker, {
+ Emoji,
+ EmojiStyle,
+ Theme as EmojiTheme,
+} from "emoji-picker-react";
+
+import { ModelType } from "../store";
+
+import BotIcon from "../icons/bot.svg";
+import BlackBotIcon from "../icons/black-bot.svg";
+
+export function getEmojiUrl(unified: string, style: EmojiStyle) {
+ return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
+}
+
+export function AvatarPicker(props: {
+ onEmojiClick: (emojiId: string) => void;
+}) {
+ return (
+
{
+ props.onEmojiClick(e.unified);
+ }}
+ />
+ );
+}
+
+export function Avatar(props: { model?: ModelType; avatar?: string }) {
+ if (props.model) {
+ return (
+
+ {props.model?.startsWith("gpt-4") ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ return (
+
+ {props.avatar && }
+
+ );
+}
+
+export function EmojiAvatar(props: { avatar: string; size?: number }) {
+ return (
+
+ );
+}
diff --git a/app/components/home.module.scss b/app/components/home.module.scss
index 7476c08f..be630e1f 100644
--- a/app/components/home.module.scss
+++ b/app/components/home.module.scss
@@ -368,17 +368,6 @@
margin-top: 5px;
}
-.user-avtar {
- height: 30px;
- width: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- border: var(--border-in-light);
- box-shadow: var(--card-shadow);
- border-radius: 10px;
-}
-
.chat-message-item {
box-sizing: border-box;
max-width: 100%;
diff --git a/app/components/model-config.tsx b/app/components/model-config.tsx
new file mode 100644
index 00000000..2b6d59f5
--- /dev/null
+++ b/app/components/model-config.tsx
@@ -0,0 +1,141 @@
+import styles from "./settings.module.scss";
+import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
+
+import Locale from "../locales";
+import { InputRange } from "./input-range";
+import { List, ListItem } from "./ui-lib";
+
+export function ModelConfigList(props: {
+ modelConfig: ModelConfig;
+ updateConfig: (updater: (config: ModelConfig) => void) => void;
+}) {
+ return (
+
+
+
+
+
+ {
+ props.updateConfig(
+ (config) =>
+ (config.temperature = ModalConfigValidator.temperature(
+ e.currentTarget.valueAsNumber,
+ )),
+ );
+ }}
+ >
+
+
+
+ props.updateConfig(
+ (config) =>
+ (config.max_tokens = ModalConfigValidator.max_tokens(
+ e.currentTarget.valueAsNumber,
+ )),
+ )
+ }
+ >
+
+
+ {
+ props.updateConfig(
+ (config) =>
+ (config.presence_penalty =
+ ModalConfigValidator.presence_penalty(
+ e.currentTarget.valueAsNumber,
+ )),
+ );
+ }}
+ >
+
+
+
+
+ props.updateConfig(
+ (config) => (config.historyMessageCount = e.target.valueAsNumber),
+ )
+ }
+ >
+
+
+
+
+ props.updateConfig(
+ (config) =>
+ (config.compressMessageLengthThreshold =
+ e.currentTarget.valueAsNumber),
+ )
+ }
+ >
+
+
+
+ props.updateConfig(
+ (config) => (config.sendMemory = e.currentTarget.checked),
+ )
+ }
+ >
+
+
+ );
+}
diff --git a/app/components/settings.module.scss b/app/components/settings.module.scss
index 9df76d32..6fb5a68b 100644
--- a/app/components/settings.module.scss
+++ b/app/components/settings.module.scss
@@ -5,16 +5,6 @@
overflow: auto;
}
-.settings-title {
- font-size: 14px;
- font-weight: bolder;
-}
-
-.settings-sub-title {
- font-size: 12px;
- font-weight: normal;
-}
-
.avatar {
cursor: pointer;
}
diff --git a/app/components/settings.tsx b/app/components/settings.tsx
index 1b2b4c7f..ffe540a9 100644
--- a/app/components/settings.tsx
+++ b/app/components/settings.tsx
@@ -1,7 +1,5 @@
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
-import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
-
import styles from "./settings.module.scss";
import ResetIcon from "../icons/reload.svg";
@@ -10,30 +8,27 @@ import CopyIcon from "../icons/copy.svg";
import ClearIcon from "../icons/clear.svg";
import EditIcon from "../icons/edit.svg";
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
+import { ModelConfigList } from "./model-config";
import { IconButton } from "./button";
import {
SubmitKey,
useChatStore,
Theme,
- ALL_MODELS,
useUpdateStore,
useAccessStore,
- ModalConfigValidator,
useAppConfig,
- ChatConfig,
- ModelConfig,
} from "../store";
-import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales";
-import { copyToClipboard, getEmojiUrl } from "../utils";
+import { copyToClipboard } from "../utils";
import Link from "next/link";
import { Path, UPDATE_URL } from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
import { useNavigate } from "react-router-dom";
+import { Avatar, AvatarPicker } from "./emoji";
function UserPromptModal(props: { onClose?: () => void }) {
const promptStore = usePromptStore();
@@ -136,148 +131,6 @@ function UserPromptModal(props: { onClose?: () => void }) {
);
}
-function SettingItem(props: {
- title: string;
- subTitle?: string;
- children: JSX.Element;
-}) {
- return (
-
-
-
{props.title}
- {props.subTitle && (
-
{props.subTitle}
- )}
-
- {props.children}
-
- );
-}
-
-export function ModelConfigList(props: {
- modelConfig: ModelConfig;
- updateConfig: (updater: (config: ModelConfig) => void) => void;
-}) {
- return (
- <>
-
-
-
-
- {
- props.updateConfig(
- (config) =>
- (config.temperature = ModalConfigValidator.temperature(
- e.currentTarget.valueAsNumber,
- )),
- );
- }}
- >
-
-
-
- props.updateConfig(
- (config) =>
- (config.max_tokens = ModalConfigValidator.max_tokens(
- e.currentTarget.valueAsNumber,
- )),
- )
- }
- >
-
-
- {
- props.updateConfig(
- (config) =>
- (config.presence_penalty =
- ModalConfigValidator.presence_penalty(
- e.currentTarget.valueAsNumber,
- )),
- );
- }}
- >
-
-
-
-
- props.updateConfig(
- (config) => (config.historyMessageCount = e.target.valueAsNumber),
- )
- }
- >
-
-
-
-
- props.updateConfig(
- (config) =>
- (config.compressMessageLengthThreshold =
- e.currentTarget.valueAsNumber),
- )
- }
- >
-
- >
- );
-}
-
export function Settings() {
const navigate = useNavigate();
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@@ -401,16 +254,13 @@ export function Settings() {
-
+
setShowEmojiPicker(false)}
content={
- {
- updateConfig((config) => (config.avatar = e.unified));
+ {
+ updateConfig((config) => (config.avatar = avatar));
setShowEmojiPicker(false);
}}
/>
@@ -421,12 +271,12 @@ export function Settings() {
className={styles.avatar}
onClick={() => setShowEmojiPicker(true)}
>
-
+
-
+
-
checkUpdate(true)}
/>
)}
-
+
-
+
-
+
-
-
- {Locale.Settings.Theme}
-
+
-
+
-
+
-
@@ -521,9 +368,9 @@ export function Settings() {
)
}
>
-
+
-
+
-
+
-
+
-
+
{enabledAccessControl ? (
-
@@ -563,12 +410,12 @@ export function Settings() {
accessStore.updateCode(e.currentTarget.value);
}}
/>
-
+
) : (
<>>
)}
-
@@ -580,9 +427,9 @@ export function Settings() {
accessStore.updateToken(e.currentTarget.value);
}}
/>
-
+
-
)}
-
+
-
@@ -622,9 +469,9 @@ export function Settings() {
)
}
>
-
+
- setShowPromptModal(true)}
/>
-
+
-
- {
- const modelConfig = { ...config.modelConfig };
- upater(modelConfig);
- config.update((config) => (config.modelConfig = modelConfig));
- }}
- />
-
+
{
+ const modelConfig = { ...config.modelConfig };
+ upater(modelConfig);
+ config.update((config) => (config.modelConfig = modelConfig));
+ }}
+ />
{shouldShowPromptModal && (
setShowPromptModal(false)} />
diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss
index 8965c06a..e3acd6d6 100644
--- a/app/components/ui-lib.module.scss
+++ b/app/components/ui-lib.module.scss
@@ -35,6 +35,16 @@
border-bottom: var(--border-in-light);
padding: 10px 20px;
animation: slide-in ease 0.6s;
+
+ .list-item-title {
+ font-size: 14px;
+ font-weight: bolder;
+ }
+
+ .list-item-sub-title {
+ font-size: 12px;
+ font-weight: normal;
+ }
}
.list {
@@ -89,6 +99,8 @@
padding: var(--modal-padding);
display: flex;
justify-content: flex-end;
+ border-top: var(--border-in-light);
+ box-shadow: var(--shadow);
.modal-actions {
display: flex;
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx
index 8e04db3a..4a92461c 100644
--- a/app/components/ui-lib.tsx
+++ b/app/components/ui-lib.tsx
@@ -33,12 +33,22 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
);
}
-export function ListItem(props: { children: JSX.Element[] }) {
- if (props.children.length > 2) {
- throw Error("Only Support Two Children");
- }
-
- return {props.children}
;
+export function ListItem(props: {
+ title: string;
+ subTitle?: string;
+ children?: JSX.Element | JSX.Element[];
+}) {
+ return (
+
+
+
{props.title}
+ {props.subTitle && (
+
{props.subTitle}
+ )}
+
+ {props.children}
+
+ );
}
export function List(props: { children: JSX.Element[] | JSX.Element }) {
@@ -63,7 +73,7 @@ export function Loading() {
interface ModalProps {
title: string;
- children?: JSX.Element;
+ children?: JSX.Element | JSX.Element[];
actions?: JSX.Element[];
onClose?: () => void;
}
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 777cea59..2e35cb30 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -39,7 +39,7 @@ const cn = {
},
Memory: {
Title: "历史摘要",
- EmptyContent: "尚未总结",
+ EmptyContent: "对话内容过短,无需总结",
Send: "启用总结并发送摘要",
Copy: "复制摘要",
Reset: "重置对话",
@@ -172,8 +172,8 @@ const cn = {
},
Context: {
Toast: (x: any) => `已设置 ${x} 条前置上下文`,
- Edit: "前置上下文和历史记忆",
- Add: "新增一条",
+ Edit: "当前对话设置",
+ Add: "新增预设对话",
},
};
diff --git a/app/store/app.ts b/app/store/chat.ts
similarity index 98%
rename from app/store/app.ts
rename to app/store/chat.ts
index 652e26f5..fcea406b 100644
--- a/app/store/app.ts
+++ b/app/store/chat.ts
@@ -11,7 +11,7 @@ import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales";
import { showToast } from "../components/ui-lib";
-import { ModelType, useAppConfig } from "./config";
+import { ModelConfig, ModelType, useAppConfig } from "./config";
export type Message = ChatCompletionResponseMessage & {
date: string;
@@ -42,16 +42,18 @@ export interface ChatStat {
export interface ChatSession {
id: number;
topic: string;
- sendMemory: boolean;
+ avatar?: string;
memoryPrompt: string;
context: Message[];
messages: Message[];
stat: ChatStat;
lastUpdate: string;
lastSummarizeIndex: number;
+
+ modelConfig: ModelConfig;
}
-const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
+export const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
export const BOT_HELLO: Message = createMessage({
role: "assistant",
content: Locale.Store.BotHello,
@@ -63,7 +65,6 @@ function createEmptySession(): ChatSession {
return {
id: Date.now(),
topic: DEFAULT_TOPIC,
- sendMemory: true,
memoryPrompt: "",
context: [],
messages: [],
@@ -74,6 +75,8 @@ function createEmptySession(): ChatSession {
},
lastUpdate: createDate,
lastSummarizeIndex: 0,
+
+ modelConfig: useAppConfig.getState().modelConfig,
};
}
diff --git a/app/store/config.ts b/app/store/config.ts
index 93733409..1e604607 100644
--- a/app/store/config.ts
+++ b/app/store/config.ts
@@ -32,6 +32,7 @@ const DEFAULT_CONFIG = {
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
+ sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
diff --git a/app/store/index.ts b/app/store/index.ts
index 7b7bbd04..0760f48c 100644
--- a/app/store/index.ts
+++ b/app/store/index.ts
@@ -1,4 +1,4 @@
-export * from "./app";
+export * from "./chat";
export * from "./update";
export * from "./access";
export * from "./config";
diff --git a/app/styles/globals.scss b/app/styles/globals.scss
index 5815d741..c5ffacbc 100644
--- a/app/styles/globals.scss
+++ b/app/styles/globals.scss
@@ -325,3 +325,14 @@ pre {
min-width: 80%;
}
}
+
+.user-avtar {
+ height: 30px;
+ width: 30px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: var(--border-in-light);
+ box-shadow: var(--card-shadow);
+ border-radius: 10px;
+}
diff --git a/app/utils.ts b/app/utils.ts
index dfec8d3e..0ebcc93a 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -1,4 +1,3 @@
-import { EmojiStyle } from "emoji-picker-react";
import { useEffect, useState } from "react";
import { showToast } from "./components/ui-lib";
import Locale from "./locales";
@@ -90,10 +89,6 @@ export function selectOrCopy(el: HTMLElement, content: string) {
return true;
}
-export function getEmojiUrl(unified: string, style: EmojiStyle) {
- return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
-}
-
function getDomContentWidth(dom: HTMLElement) {
const style = window.getComputedStyle(dom);
const paddingWidth =