diff --git a/app/components/button.module.scss b/app/components/button.module.scss
index b882a0c1f..88da97481 100644
--- a/app/components/button.module.scss
+++ b/app/components/button.module.scss
@@ -6,19 +6,21 @@
justify-content: center;
padding: 10px;
- box-shadow: var(--card-shadow);
cursor: pointer;
transition: all 0.3s ease;
overflow: hidden;
user-select: none;
}
+.shadow {
+ box-shadow: var(--card-shadow);
+}
+
.border {
border: var(--border-in-light);
}
.icon-button:hover {
- filter: brightness(0.9);
border-color: var(--primary);
}
@@ -36,25 +38,7 @@
}
}
-@mixin dark-button {
- div:not(:global(.no-dark))>.icon-button-icon {
- filter: invert(0.5);
- }
-
- .icon-button:hover {
- filter: brightness(1.2);
- }
-}
-
-:global(.dark) {
- @include dark-button;
-}
-
-@media (prefers-color-scheme: dark) {
- @include dark-button;
-}
-
.icon-button-text {
margin-left: 5px;
font-size: 12px;
-}
\ No newline at end of file
+}
diff --git a/app/components/button.tsx b/app/components/button.tsx
index 43b699b68..f40a4e8fd 100644
--- a/app/components/button.tsx
+++ b/app/components/button.tsx
@@ -7,6 +7,7 @@ export function IconButton(props: {
icon: JSX.Element;
text?: string;
bordered?: boolean;
+ shadow?: boolean;
className?: string;
title?: string;
}) {
@@ -14,10 +15,13 @@ export function IconButton(props: {
{props.icon}
{props.text && (
diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx
index 5a74ff151..8ad2b7dc0 100644
--- a/app/components/chat-list.tsx
+++ b/app/components/chat-list.tsx
@@ -11,6 +11,7 @@ import {
} from "../store";
import Locale from "../locales";
+import { isMobileScreen } from "../utils";
export function ChatItem(props: {
onClick?: () => void;
@@ -61,7 +62,10 @@ export function ChatList() {
key={i}
selected={i === selectedIndex}
onClick={() => selectSession(i)}
- onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)}
+ onDelete={() =>
+ (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
+ removeSession(i)
+ }
/>
))}
diff --git a/app/components/chat.module.scss b/app/components/chat.module.scss
new file mode 100644
index 000000000..b52baa12d
--- /dev/null
+++ b/app/components/chat.module.scss
@@ -0,0 +1,71 @@
+.prompt-toast {
+ position: absolute;
+ bottom: -50px;
+ z-index: 999;
+ display: flex;
+ justify-content: center;
+ width: calc(100% - 40px);
+
+ .prompt-toast-inner {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 12px;
+ background-color: var(--white);
+ color: var(--black);
+
+ border: var(--border-in-light);
+ box-shadow: var(--card-shadow);
+ padding: 10px 20px;
+ border-radius: 100px;
+
+ .prompt-toast-content {
+ margin-left: 10px;
+ }
+ }
+}
+
+.context-prompt {
+ .context-prompt-row {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ margin-bottom: 10px;
+
+ .context-role {
+ margin-right: 10px;
+ }
+
+ .context-content {
+ flex: 1;
+ max-width: 100%;
+ text-align: left;
+ }
+
+ .context-delete-button {
+ margin-left: 10px;
+ }
+ }
+
+ .context-prompt-button {
+ flex: 1;
+ }
+}
+
+.memory-prompt {
+ margin-top: 20px;
+
+ .memory-prompt-title {
+ font-size: 12px;
+ font-weight: bold;
+ margin-bottom: 10px;
+ }
+
+ .memory-prompt-content {
+ background-color: var(--gray);
+ border-radius: 6px;
+ padding: 10px;
+ font-size: 12px;
+ user-select: text;
+ }
+}
diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 7300549ca..2294f39b3 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -9,6 +9,8 @@ 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 AddIcon from "../icons/add.svg";
+import DeleteIcon from "../icons/delete.svg";
import {
Message,
@@ -16,6 +18,7 @@ import {
useChatStore,
ChatSession,
BOT_HELLO,
+ ROLES,
} from "../store";
import {
@@ -33,8 +36,9 @@ import Locale from "../locales";
import { IconButton } from "./button";
import styles from "./home.module.scss";
+import chatStyle from "./chat.module.scss";
-import { showModal, showToast } from "./ui-lib";
+import { Modal, showModal, showToast } from "./ui-lib";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => ,
@@ -94,26 +98,130 @@ function exportMessages(messages: Message[], topic: string) {
});
}
-function showMemoryPrompt(session: ChatSession) {
- showModal({
- title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`,
- children: (
-
-
- {session.memoryPrompt || Locale.Memory.EmptyContent}
-
+function PromptToast(props: {
+ showModal: boolean;
+ setShowModal: (_: boolean) => void;
+}) {
+ const chatStore = useChatStore();
+ const session = chatStore.currentSession();
+ const context = session.context;
+
+ const addContextPrompt = (prompt: Message) => {
+ chatStore.updateCurrentSession((session) => {
+ session.context.push(prompt);
+ });
+ };
+
+ const removeContextPrompt = (i: number) => {
+ chatStore.updateCurrentSession((session) => {
+ session.context.splice(i, 1);
+ });
+ };
+
+ const updateContextPrompt = (i: number, prompt: Message) => {
+ chatStore.updateCurrentSession((session) => {
+ session.context[i] = prompt;
+ });
+ };
+
+ return (
+
+
props.setShowModal(true)}
+ >
+
+
+ 已设置 {context.length} 条前置上下文
+
- ),
- actions: [
-
}
- bordered
- text={Locale.Memory.Copy}
- onClick={() => copyToClipboard(session.memoryPrompt)}
- />,
- ],
- });
+ {props.showModal && (
+
+
props.setShowModal(false)}
+ actions={[
+ }
+ bordered
+ text={Locale.Memory.Copy}
+ onClick={() => copyToClipboard(session.memoryPrompt)}
+ />,
+ ]}
+ >
+ <>
+ {" "}
+
+ {context.map((c, i) => (
+
+
+
+ updateContextPrompt(i, {
+ ...c,
+ content: e.target.value as any,
+ })
+ }
+ >
+ }
+ className={chatStyle["context-delete-button"]}
+ onClick={() => removeContextPrompt(i)}
+ />
+
+ ))}
+
+
+ }
+ text="新增"
+ 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}
+
+
+ >
+
+
+ )}
+
+ );
}
function useSubmitHandler() {
@@ -172,9 +280,8 @@ function useScrollToBottom() {
// auto scroll
useLayoutEffect(() => {
const dom = scrollRef.current;
-
if (dom && autoScroll) {
- dom.scrollTop = dom.scrollHeight;
+ setTimeout(() => (dom.scrollTop = dom.scrollHeight), 500);
}
});
@@ -243,8 +350,12 @@ export function Chat(props: {
setPromptHints([]);
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion
- if (text.startsWith("/") && text.length > 1) {
- onSearch(text.slice(1));
+ if (text.startsWith("/")) {
+ let searchText = text.slice(1);
+ if (searchText.length === 0) {
+ searchText = " ";
+ }
+ onSearch(searchText);
}
}
};
@@ -299,8 +410,18 @@ export function Chat(props: {
const config = useChatStore((state) => state.config);
+ const context: RenderMessage[] = session.context.slice();
+
+ if (
+ context.length === 0 &&
+ session.messages.at(0)?.content !== BOT_HELLO.content
+ ) {
+ context.push(BOT_HELLO);
+ }
+
// preview messages
- const messages = (session.messages as RenderMessage[])
+ const messages = context
+ .concat(session.messages as RenderMessage[])
.concat(
isLoading
? [
@@ -326,6 +447,8 @@ export function Chat(props: {
: [],
);
+ const [showPromptModal, setShowPromptModal] = useState(false);
+
return (
@@ -365,7 +488,7 @@ export function Chat(props: {
bordered
title={Locale.Chat.Actions.CompressedHistory}
onClick={() => {
- showMemoryPrompt(session);
+ setShowPromptModal(true);
}}
/>
@@ -380,6 +503,11 @@ export function Chat(props: {
/>
+
+
@@ -402,7 +530,10 @@ export function Chat(props: {
{Locale.Chat.Typing}
)}
-
+
inputRef.current?.blur()}
+ >
{!isUser &&
!(message.preview || message.content.length === 0) && (
@@ -467,7 +598,7 @@ export function Chat(props: {
ref={inputRef}
className={styles["chat-input"]}
placeholder={Locale.Chat.Input(submitKey)}
- rows={4}
+ rows={2}
onInput={(e) => onInput(e.currentTarget.value)}
value={userInput}
onKeyDown={onInputKeyDown}
diff --git a/app/components/home.module.scss b/app/components/home.module.scss
index 764805d80..24b1f1bf6 100644
--- a/app/components/home.module.scss
+++ b/app/components/home.module.scss
@@ -218,6 +218,7 @@
flex: 1;
overflow: auto;
padding: 20px;
+ position: relative;
}
.chat-body-title {
diff --git a/app/components/home.tsx b/app/components/home.tsx
index f1ce54ad3..13db93e29 100644
--- a/app/components/home.tsx
+++ b/app/components/home.tsx
@@ -149,11 +149,12 @@ export function Home() {
setOpenSettings(true);
setShowSideBar(false);
}}
+ shadow
/>
@@ -165,6 +166,7 @@ export function Home() {
createNewSession();
setShowSideBar(false);
}}
+ shadow
/>
diff --git a/app/components/window.scss b/app/components/window.scss
index c17271158..d89c9eb10 100644
--- a/app/components/window.scss
+++ b/app/components/window.scss
@@ -1,6 +1,7 @@
.window-header {
padding: 14px 20px;
border-bottom: rgba(0, 0, 0, 0.1) 1px solid;
+ position: relative;
display: flex;
justify-content: space-between;
@@ -32,4 +33,4 @@
.window-action-button {
margin-left: 10px;
-}
\ No newline at end of file
+}
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index 62be467bd..afdcba43b 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -138,7 +138,7 @@ const cn = {
Topic:
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”",
Summarize:
- "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内",
+ "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 200 字以内",
},
ConfirmClearAll: "确认清除所有聊天、设置数据?",
},
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 98fa7404c..87b73b49a 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -142,7 +142,7 @@ const en: LocaleType = {
Topic:
"Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.",
Summarize:
- "Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.",
+ "Summarize our discussion briefly in 200 words or less to use as a prompt for future context.",
},
ConfirmClearAll: "Confirm to clear all chat and setting data?",
},
diff --git a/app/locales/es.ts b/app/locales/es.ts
index fca7202d1..f195969b3 100644
--- a/app/locales/es.ts
+++ b/app/locales/es.ts
@@ -142,7 +142,7 @@ const es: LocaleType = {
Topic:
"Por favor, genera un título de cuatro a cinco palabras que resuma nuestra conversación sin ningún inicio, puntuación, comillas, puntos, símbolos o texto adicional. Elimina las comillas que lo envuelven.",
Summarize:
- "Resuma nuestra discusión brevemente en 50 caracteres o menos para usarlo como un recordatorio para futuros contextos.",
+ "Resuma nuestra discusión brevemente en 200 caracteres o menos para usarlo como un recordatorio para futuros contextos.",
},
ConfirmClearAll:
"¿Confirmar para borrar todos los datos de chat y configuración?",
diff --git a/app/locales/tw.ts b/app/locales/tw.ts
index 271562834..371bca347 100644
--- a/app/locales/tw.ts
+++ b/app/locales/tw.ts
@@ -137,7 +137,7 @@ const tw: LocaleType = {
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
Summarize:
- "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 50 字以內",
+ "簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內",
},
ConfirmClearAll: "確認清除所有對話、設定數據?",
},
diff --git a/app/store/app.ts b/app/store/app.ts
index 7c2b57f1b..3e98757cb 100644
--- a/app/store/app.ts
+++ b/app/store/app.ts
@@ -53,6 +53,8 @@ export interface ChatConfig {
export type ModelConfig = ChatConfig["modelConfig"];
+export const ROLES: Message["role"][] = ["system", "user", "assistant"];
+
const ENABLE_GPT4 = true;
export const ALL_MODELS = [
@@ -151,6 +153,7 @@ export interface ChatSession {
id: number;
topic: string;
memoryPrompt: string;
+ context: Message[];
messages: Message[];
stat: ChatStat;
lastUpdate: string;
@@ -158,7 +161,7 @@ export interface ChatSession {
}
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
-export const BOT_HELLO = {
+export const BOT_HELLO: Message = {
role: "assistant",
content: Locale.Store.BotHello,
date: "",
@@ -171,6 +174,7 @@ function createEmptySession(): ChatSession {
id: Date.now(),
topic: DEFAULT_TOPIC,
memoryPrompt: "",
+ context: [],
messages: [],
stat: {
tokenCount: 0,
@@ -380,16 +384,18 @@ export const useChatStore = create()(
const session = get().currentSession();
const config = get().config;
const n = session.messages.length;
- const recentMessages = session.messages.slice(
- Math.max(0, n - config.historyMessageCount),
- );
- const memoryPrompt = get().getMemoryPrompt();
+ const context = session.context.slice();
- if (session.memoryPrompt) {
- recentMessages.unshift(memoryPrompt);
+ if (session.memoryPrompt && session.memoryPrompt.length > 0) {
+ const memoryPrompt = get().getMemoryPrompt();
+ context.push(memoryPrompt);
}
+ const recentMessages = context.concat(
+ session.messages.slice(Math.max(0, n - config.historyMessageCount)),
+ );
+
return recentMessages;
},
@@ -427,11 +433,13 @@ export const useChatStore = create()(
let toBeSummarizedMsgs = session.messages.slice(
session.lastSummarizeIndex,
);
+
const historyMsgLength = countMessages(toBeSummarizedMsgs);
- if (historyMsgLength > 4000) {
+ if (historyMsgLength > get().config?.modelConfig?.max_tokens ?? 4000) {
+ const n = toBeSummarizedMsgs.length;
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
- -config.historyMessageCount,
+ Math.max(0, n - config.historyMessageCount),
);
}
@@ -494,7 +502,16 @@ export const useChatStore = create()(
}),
{
name: LOCAL_KEY,
- version: 1,
+ version: 1.1,
+ migrate(persistedState, version) {
+ const state = persistedState as ChatStore;
+
+ if (version === 1) {
+ state.sessions.forEach((s) => (s.context = []));
+ }
+
+ return state;
+ },
},
),
);
diff --git a/app/styles/globals.scss b/app/styles/globals.scss
index e14ee684c..e179dcf37 100644
--- a/app/styles/globals.scss
+++ b/app/styles/globals.scss
@@ -117,7 +117,7 @@ body {
select {
border: var(--border-in-light);
- padding: 8px 10px;
+ padding: 10px;
border-radius: 10px;
appearance: none;
cursor: pointer;
@@ -188,7 +188,7 @@ input[type="text"] {
appearance: none;
border-radius: 10px;
border: var(--border-in-light);
- height: 32px;
+ height: 36px;
box-sizing: border-box;
background: var(--white);
color: var(--black);
@@ -256,3 +256,15 @@ pre {
}
}
}
+
+.clickable {
+ cursor: pointer;
+
+ div:not(.no-dark) > svg {
+ filter: invert(0.5);
+ }
+
+ &:hover {
+ filter: brightness(0.9);
+ }
+}