Merge remote-tracking branch 'upstream/main'

This commit is contained in:
ZhaoLiu
2023-05-05 22:27:11 +08:00
76 changed files with 3404 additions and 1180 deletions

View File

@@ -1,11 +1,15 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { StoreKey } from "../constant";
import { BOT_HELLO } from "./chat";
export interface AccessControlStore {
accessCode: string;
token: string;
needCode: boolean;
hideUserApiKey: boolean;
openaiUrl: string;
updateToken: (_: string) => void;
updateCode: (_: string) => void;
@@ -14,8 +18,6 @@ export interface AccessControlStore {
fetch: () => void;
}
export const ACCESS_KEY = "access-control";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
export const useAccessStore = create<AccessControlStore>()(
@@ -24,16 +26,19 @@ export const useAccessStore = create<AccessControlStore>()(
token: "",
accessCode: "",
needCode: true,
hideUserApiKey: false,
openaiUrl: "/api/openai/",
enabledAccessControl() {
get().fetch();
return get().needCode;
},
updateCode(code: string) {
set((state) => ({ accessCode: code }));
set(() => ({ accessCode: code }));
},
updateToken(token: string) {
set((state) => ({ token }));
set(() => ({ token }));
},
isAuthorized() {
// has token or has code or disabled access control
@@ -52,6 +57,10 @@ export const useAccessStore = create<AccessControlStore>()(
.then((res: DangerConfig) => {
console.log("[Config] got config from server", res);
set(() => ({ ...res }));
if ((res as any).botHello) {
BOT_HELLO.content = (res as any).botHello;
}
})
.catch(() => {
console.error("[Config] failed to fetch config");
@@ -62,7 +71,7 @@ export const useAccessStore = create<AccessControlStore>()(
},
}),
{
name: ACCESS_KEY,
name: StoreKey.Access,
version: 1,
},
),

View File

@@ -11,7 +11,9 @@ import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales";
import { showToast } from "../components/ui-lib";
import { ModelType, useAppConfig } from "./config";
import { DEFAULT_CONFIG, ModelConfig, ModelType, useAppConfig } from "./config";
import { createEmptyMask, Mask } from "./mask";
import { StoreKey } from "../constant";
export type Message = ChatCompletionResponseMessage & {
date: string;
@@ -41,51 +43,50 @@ export interface ChatStat {
export interface ChatSession {
id: number;
topic: string;
sendMemory: boolean;
memoryPrompt: string;
context: Message[];
messages: Message[];
stat: ChatStat;
lastUpdate: string;
lastUpdate: number;
lastSummarizeIndex: number;
mask: Mask;
}
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,
});
function createEmptySession(): ChatSession {
const createDate = new Date().toLocaleString();
return {
id: Date.now(),
id: Date.now() + Math.random(),
topic: DEFAULT_TOPIC,
sendMemory: true,
memoryPrompt: "",
context: [],
messages: [],
stat: {
tokenCount: 0,
wordCount: 0,
charCount: 0,
},
lastUpdate: createDate,
lastUpdate: Date.now(),
lastSummarizeIndex: 0,
mask: createEmptyMask(),
};
}
interface ChatStore {
sessions: ChatSession[];
currentSessionIndex: number;
globalId: number;
clearSessions: () => void;
removeSession: (index: number) => void;
moveSession: (from: number, to: number) => void;
selectSession: (index: number) => void;
newSession: () => void;
deleteSession: (index?: number) => void;
newSession: (mask?: Mask) => void;
deleteSession: (index: number) => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>;
@@ -108,13 +109,12 @@ function countMessages(msgs: Message[]) {
return msgs.reduce((pre, cur) => pre + cur.content.length, 0);
}
const LOCAL_KEY = "chat-next-web-store";
export const useChatStore = create<ChatStore>()(
persist(
(set, get) => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
globalId: 0,
clearSessions() {
set(() => ({
@@ -129,31 +129,6 @@ export const useChatStore = create<ChatStore>()(
});
},
removeSession(index: number) {
set((state) => {
let nextIndex = state.currentSessionIndex;
const sessions = state.sessions;
if (sessions.length === 1) {
return {
currentSessionIndex: 0,
sessions: [createEmptySession()],
};
}
sessions.splice(index, 1);
if (nextIndex === index) {
nextIndex -= 1;
}
return {
currentSessionIndex: nextIndex,
sessions,
};
});
},
moveSession(from: number, to: number) {
set((state) => {
const { sessions, currentSessionIndex: oldIndex } = state;
@@ -179,38 +154,63 @@ export const useChatStore = create<ChatStore>()(
});
},
newSession() {
newSession(mask) {
const session = createEmptySession();
set(() => ({ globalId: get().globalId + 1 }));
session.id = get().globalId;
if (mask) {
session.mask = { ...mask };
session.topic = mask.name;
}
set((state) => ({
currentSessionIndex: 0,
sessions: [createEmptySession()].concat(state.sessions),
sessions: [session].concat(state.sessions),
}));
},
deleteSession(i?: number) {
const deletedSession = get().currentSession();
const index = i ?? get().currentSessionIndex;
const isLastSession = get().sessions.length === 1;
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
get().removeSession(index);
deleteSession(index) {
const deletingLastSession = get().sessions.length === 1;
const deletedSession = get().sessions.at(index);
showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert,
onClick() {
set((state) => ({
sessions: state.sessions
.slice(0, index)
.concat([deletedSession])
.concat(
state.sessions.slice(index + Number(isLastSession)),
),
}));
},
},
5000,
);
if (!deletedSession) return;
const sessions = get().sessions.slice();
sessions.splice(index, 1);
let nextIndex = Math.min(
get().currentSessionIndex,
sessions.length - 1,
);
if (deletingLastSession) {
nextIndex = 0;
sessions.push(createEmptySession());
}
// for undo delete action
const restoreState = {
currentSessionIndex: get().currentSessionIndex,
sessions: get().sessions.slice(),
};
set(() => ({
currentSessionIndex: nextIndex,
sessions,
}));
showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert,
onClick() {
set(() => restoreState);
},
},
5000,
);
},
currentSession() {
@@ -229,13 +229,16 @@ export const useChatStore = create<ChatStore>()(
onNewMessage(message) {
get().updateCurrentSession((session) => {
session.lastUpdate = new Date().toLocaleString();
session.lastUpdate = Date.now();
});
get().updateStat(message);
get().summarizeSession();
},
async onUserInput(content) {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
const userMessage: Message = createMessage({
role: "user",
content,
@@ -245,7 +248,7 @@ export const useChatStore = create<ChatStore>()(
role: "assistant",
streaming: true,
id: userMessage.id! + 1,
model: useAppConfig.getState().modelConfig.model,
model: modelConfig.model,
});
// get recent messages
@@ -279,14 +282,16 @@ export const useChatStore = create<ChatStore>()(
}
},
onError(error, statusCode) {
const isAborted = error.message.includes("aborted");
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
} else if (!error.message.includes("aborted")) {
} else if (!isAborted) {
botMessage.content += "\n\n" + Locale.Store.Error;
}
botMessage.streaming = false;
userMessage.isError = true;
botMessage.isError = true;
userMessage.isError = !isAborted;
botMessage.isError = !isAborted;
set(() => ({}));
ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex);
},
@@ -298,8 +303,7 @@ export const useChatStore = create<ChatStore>()(
controller,
);
},
filterBot: !useAppConfig.getState().sendBotMessages,
modelConfig: useAppConfig.getState().modelConfig,
modelConfig: { ...modelConfig },
});
},
@@ -308,22 +312,25 @@ export const useChatStore = create<ChatStore>()(
return {
role: "system",
content: Locale.Store.Prompt.History(session.memoryPrompt),
content:
session.memoryPrompt.length > 0
? Locale.Store.Prompt.History(session.memoryPrompt)
: "",
date: "",
} as Message;
},
getMessagesWithMemory() {
const session = get().currentSession();
const config = useAppConfig.getState();
const modelConfig = session.mask.modelConfig;
const messages = session.messages.filter((msg) => !msg.isError);
const n = messages.length;
const context = session.context.slice();
const context = session.mask.context.slice();
// long term memory
if (
session.sendMemory &&
modelConfig.sendMemory &&
session.memoryPrompt &&
session.memoryPrompt.length > 0
) {
@@ -334,14 +341,14 @@ export const useChatStore = create<ChatStore>()(
// get short term and unmemoried long term memory
const shortTermMemoryMessageIndex = Math.max(
0,
n - config.historyMessageCount,
n - modelConfig.historyMessageCount,
);
const longTermMemoryMessageIndex = session.lastSummarizeIndex;
const oldestIndex = Math.max(
shortTermMemoryMessageIndex,
longTermMemoryMessageIndex,
);
const threshold = config.compressMessageLengthThreshold;
const threshold = modelConfig.compressMessageLengthThreshold;
// get recent messages as many as possible
const reversedRecentMessages = [];
@@ -400,17 +407,17 @@ export const useChatStore = create<ChatStore>()(
});
}
const config = useAppConfig.getState();
const modelConfig = session.mask.modelConfig;
let toBeSummarizedMsgs = session.messages.slice(
session.lastSummarizeIndex,
);
const historyMsgLength = countMessages(toBeSummarizedMsgs);
if (historyMsgLength > config?.modelConfig?.max_tokens ?? 4000) {
if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
const n = toBeSummarizedMsgs.length;
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
Math.max(0, n - config.historyMessageCount),
Math.max(0, n - modelConfig.historyMessageCount),
);
}
@@ -423,12 +430,12 @@ export const useChatStore = create<ChatStore>()(
"[Chat History] ",
toBeSummarizedMsgs,
historyMsgLength,
config.compressMessageLengthThreshold,
modelConfig.compressMessageLengthThreshold,
);
if (
historyMsgLength > config.compressMessageLengthThreshold &&
session.sendMemory
historyMsgLength > modelConfig.compressMessageLengthThreshold &&
session.mask.modelConfig.sendMemory
) {
requestChatStream(
toBeSummarizedMsgs.concat({
@@ -437,8 +444,7 @@ export const useChatStore = create<ChatStore>()(
date: "",
}),
{
filterBot: false,
model: "gpt-3.5-turbo",
overrideModel: "gpt-3.5-turbo",
onMessage(message, done) {
session.memoryPrompt = message;
if (done) {
@@ -469,27 +475,34 @@ export const useChatStore = create<ChatStore>()(
},
clearAllData() {
if (confirm(Locale.Store.ConfirmClearAll)) {
localStorage.clear();
location.reload();
}
localStorage.clear();
location.reload();
},
}),
{
name: LOCAL_KEY,
version: 1.2,
name: StoreKey.Chat,
version: 2,
migrate(persistedState, version) {
const state = persistedState as ChatStore;
const state = persistedState as any;
const newState = JSON.parse(JSON.stringify(state)) as ChatStore;
if (version === 1) {
state.sessions.forEach((s) => (s.context = []));
if (version < 2) {
newState.globalId = 0;
newState.sessions = [];
const oldSessions = state.sessions;
for (const oldSession of oldSessions) {
const newSession = createEmptySession();
newSession.topic = oldSession.topic;
newSession.messages = [...oldSession.messages];
newSession.mask.modelConfig.sendMemory = true;
newSession.mask.modelConfig.historyMessageCount = 4;
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
newState.sessions.push(newSession);
}
}
if (version < 1.2) {
state.sessions.forEach((s) => (s.sendMemory = true));
}
return state;
return newState;
},
},
),

View File

@@ -1,5 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { StoreKey } from "../constant";
export enum SubmitKey {
Enter = "Enter",
@@ -15,11 +16,8 @@ export enum Theme {
Light = "light",
}
const DEFAULT_CONFIG = {
historyMessageCount: 5,
compressMessageLengthThreshold: 1000,
sendBotMessages: true as boolean,
submitKey: SubmitKey.Enter as SubmitKey,
export const DEFAULT_CONFIG = {
submitKey: SubmitKey.CtrlEnter as SubmitKey,
avatar: "1f437",
fontSize: 14,
theme: Theme.Auto as Theme,
@@ -29,11 +27,16 @@ const DEFAULT_CONFIG = {
disablePromptHint: false,
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
modelConfig: {
model: "gpt-3.5-turbo" as ModelType,
temperature: 0.8,
max_tokens: 2000,
presence_penalty: 0,
sendMemory: true,
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
},
};
@@ -107,12 +110,10 @@ export const ModalConfigValidator = {
return limitNumber(x, -2, 2, 0);
},
temperature(x: number) {
return limitNumber(x, 0, 2, 1);
return limitNumber(x, 0, 1, 1);
},
};
const CONFIG_KEY = "app-config";
export const useAppConfig = create<ChatConfigStore>()(
persist(
(set, get) => ({
@@ -129,7 +130,19 @@ export const useAppConfig = create<ChatConfigStore>()(
},
}),
{
name: CONFIG_KEY,
name: StoreKey.Config,
version: 2,
migrate(persistedState, version) {
if (version === 2) return persistedState as any;
const state = persistedState as ChatConfig;
state.modelConfig.sendMemory = true;
state.modelConfig.historyMessageCount = 4;
state.modelConfig.compressMessageLengthThreshold = 1000;
state.dontShowMaskSplashScreen = false;
return state;
},
},
),
);

View File

@@ -1,4 +1,4 @@
export * from "./app";
export * from "./chat";
export * from "./update";
export * from "./access";
export * from "./config";

100
app/store/mask.ts Normal file
View File

@@ -0,0 +1,100 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, Message } from "./chat";
import { ModelConfig, ModelType, useAppConfig } from "./config";
import { StoreKey } from "../constant";
export type Mask = {
id: number;
avatar: string;
name: string;
context: Message[];
modelConfig: ModelConfig;
lang: Lang;
builtin: boolean;
};
export const DEFAULT_MASK_STATE = {
masks: {} as Record<number, Mask>,
globalMaskId: 0,
};
export type MaskState = typeof DEFAULT_MASK_STATE;
type MaskStore = MaskState & {
create: (mask?: Partial<Mask>) => Mask;
update: (id: number, updater: (mask: Mask) => void) => void;
delete: (id: number) => void;
search: (text: string) => Mask[];
get: (id?: number) => Mask | null;
getAll: () => Mask[];
};
export const DEFAULT_MASK_ID = 1145141919810;
export const DEFAULT_MASK_AVATAR = "gpt-bot";
export const createEmptyMask = () =>
({
id: DEFAULT_MASK_ID,
avatar: DEFAULT_MASK_AVATAR,
name: DEFAULT_TOPIC,
context: [],
modelConfig: { ...useAppConfig.getState().modelConfig },
lang: getLang(),
builtin: false,
} as Mask);
export const useMaskStore = create<MaskStore>()(
persist(
(set, get) => ({
...DEFAULT_MASK_STATE,
create(mask) {
set(() => ({ globalMaskId: get().globalMaskId + 1 }));
const id = get().globalMaskId;
const masks = get().masks;
masks[id] = {
...createEmptyMask(),
...mask,
id,
builtin: false,
};
set(() => ({ masks }));
return masks[id];
},
update(id, updater) {
const masks = get().masks;
const mask = masks[id];
if (!mask) return;
const updateMask = { ...mask };
updater(updateMask);
masks[id] = updateMask;
set(() => ({ masks }));
},
delete(id) {
const masks = get().masks;
delete masks[id];
set(() => ({ masks }));
},
get(id) {
return get().masks[id ?? 1145141919810];
},
getAll() {
const userMasks = Object.values(get().masks).sort(
(a, b) => b.id - a.id,
);
return userMasks.concat(BUILTIN_MASKS);
},
search(text) {
return Object.values(get().masks);
},
}),
{
name: StoreKey.Mask,
version: 2,
},
),
);

View File

@@ -2,6 +2,7 @@ import { create } from "zustand";
import { persist } from "zustand/middleware";
import Fuse from "fuse.js";
import { getLang } from "../locales";
import { StoreKey } from "../constant";
export interface Prompt {
id?: number;
@@ -16,15 +17,14 @@ export interface PromptStore {
prompts: Record<number, Prompt>;
add: (prompt: Prompt) => number;
get: (id: number) => Prompt | undefined;
remove: (id: number) => void;
search: (text: string) => Prompt[];
update: (id: number, updater: (prompt: Prompt) => void) => void;
getUserPrompts: () => Prompt[];
updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void;
}
export const PROMPT_KEY = "prompt-store";
export const SearchService = {
ready: false,
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
@@ -82,6 +82,16 @@ export const usePromptStore = create<PromptStore>()(
return prompt.id!;
},
get(id) {
const targetPrompt = get().prompts[id];
if (!targetPrompt) {
return SearchService.builtinPrompts.find((v) => v.id === id);
}
return targetPrompt;
},
remove(id) {
const prompts = get().prompts;
delete prompts[id];
@@ -99,7 +109,7 @@ export const usePromptStore = create<PromptStore>()(
return userPrompts;
},
updateUserPrompts(id: number, updater) {
update(id: number, updater) {
const prompt = get().prompts[id] ?? {
title: "",
content: "",
@@ -123,7 +133,7 @@ export const usePromptStore = create<PromptStore>()(
},
}),
{
name: PROMPT_KEY,
name: StoreKey.Prompt,
version: 1,
onRehydrateStorage(state) {
const PROMPT_URL = "./prompts.json";

View File

@@ -1,6 +1,6 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
import { requestUsage } from "../requests";
export interface UpdateStore {
@@ -16,8 +16,6 @@ export interface UpdateStore {
updateUsage: (force?: boolean) => Promise<void>;
}
export const UPDATE_KEY = "chat-update";
function queryMeta(key: string, defaultValue?: string): string {
let ret: string;
if (document) {
@@ -55,10 +53,9 @@ export const useUpdateStore = create<UpdateStore>()(
}));
try {
// const data = await (await fetch(FETCH_TAG_URL)).json();
// const remoteId = data[0].name as string;
const data = await (await fetch(FETCH_COMMIT_URL)).json();
const remoteId = (data[0].sha as string).substring(0, 7);
const remoteCommitTime = data[0].commit.committer.date;
const remoteId = new Date(remoteCommitTime).getTime().toString();
set(() => ({
remoteVersion: remoteId,
}));
@@ -84,7 +81,7 @@ export const useUpdateStore = create<UpdateStore>()(
},
}),
{
name: UPDATE_KEY,
name: StoreKey.Update,
version: 1,
},
),