ChatGPT-Next-Web/app/store/app.ts

641 lines
16 KiB
TypeScript
Raw Normal View History

2023-03-10 18:25:33 +00:00
import { create } from "zustand";
import { persist } from "zustand/middleware";
2023-03-09 17:01:40 +00:00
import { type ChatCompletionResponseMessage } from "openai";
2023-03-26 10:59:09 +00:00
import {
ControllerPool,
requestChatStream,
requestWithPrompt,
} from "../requests";
2023-04-06 16:14:27 +00:00
import { isMobileScreen, trimTopic } from "../utils";
2023-03-09 17:01:40 +00:00
import Locale from "../locales";
2023-04-06 16:14:27 +00:00
import { showToast } from "../components/ui-lib";
2023-03-20 16:17:45 +00:00
2023-03-10 18:25:33 +00:00
export type Message = ChatCompletionResponseMessage & {
date: string;
2023-03-11 12:54:24 +00:00
streaming?: boolean;
isError?: boolean;
2023-04-05 19:19:33 +00:00
id?: number;
model?: ModelType;
2023-03-10 18:25:33 +00:00
};
2023-03-09 17:01:40 +00:00
2023-04-05 19:19:33 +00:00
export function createMessage(override: Partial<Message>): Message {
return {
id: Date.now(),
date: new Date().toLocaleString(),
role: "user",
content: "",
...override,
};
}
2023-03-11 17:14:07 +00:00
export enum SubmitKey {
Enter = "Enter",
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
MetaEnter = "Meta + Enter",
2023-03-11 17:14:07 +00:00
}
2023-03-12 19:06:21 +00:00
export enum Theme {
Auto = "auto",
Dark = "dark",
Light = "light",
}
2023-03-19 16:09:30 +00:00
export interface ChatConfig {
2023-03-11 17:14:07 +00:00
historyMessageCount: number; // -1 means all
2023-03-19 15:13:10 +00:00
compressMessageLengthThreshold: number;
2023-03-11 17:14:07 +00:00
sendBotMessages: boolean; // send bot's message or not
submitKey: SubmitKey;
avatar: string;
2023-03-28 06:37:44 +00:00
fontSize: number;
2023-03-12 19:06:21 +00:00
theme: Theme;
2023-03-12 19:21:48 +00:00
tightBorder: boolean;
sendPreviewBubble: boolean;
2023-04-09 16:54:17 +00:00
sidebarWidth: number;
2023-03-21 16:20:32 +00:00
2023-03-28 17:30:11 +00:00
disablePromptHint: boolean;
2023-03-21 16:20:32 +00:00
modelConfig: {
model: ModelType;
2023-03-21 16:20:32 +00:00
temperature: number;
max_tokens: number;
presence_penalty: number;
};
}
export type ModelConfig = ChatConfig["modelConfig"];
export const ROLES: Message["role"][] = ["system", "user", "assistant"];
2023-03-26 12:32:22 +00:00
const ENABLE_GPT4 = true;
2023-03-21 16:20:32 +00:00
export const ALL_MODELS = [
{
name: "gpt-4",
2023-03-26 12:32:22 +00:00
available: ENABLE_GPT4,
2023-03-21 16:20:32 +00:00
},
{
name: "gpt-4-0314",
2023-03-26 12:32:22 +00:00
available: ENABLE_GPT4,
2023-03-21 16:20:32 +00:00
},
{
name: "gpt-4-32k",
2023-03-26 12:32:22 +00:00
available: ENABLE_GPT4,
2023-03-21 16:20:32 +00:00
},
{
name: "gpt-4-32k-0314",
2023-03-26 12:32:22 +00:00
available: ENABLE_GPT4,
2023-03-21 16:20:32 +00:00
},
{
name: "gpt-3.5-turbo",
available: true,
},
{
name: "gpt-3.5-turbo-0301",
available: true,
},
] as const;
export type ModelType = (typeof ALL_MODELS)[number]["name"];
2023-03-21 16:20:32 +00:00
2023-04-03 17:05:33 +00:00
export function limitNumber(
x: number,
min: number,
max: number,
2023-04-18 03:44:15 +00:00
defaultValue: number,
2023-04-03 17:05:33 +00:00
) {
if (typeof x !== "number" || isNaN(x)) {
return defaultValue;
}
return Math.min(max, Math.max(min, x));
2023-03-21 16:20:32 +00:00
}
2023-04-03 17:05:33 +00:00
export function limitModel(name: string) {
return ALL_MODELS.some((m) => m.name === name && m.available)
? name
: ALL_MODELS[4].name;
2023-03-21 16:20:32 +00:00
}
2023-04-03 17:05:33 +00:00
export const ModalConfigValidator = {
model(x: string) {
2023-04-20 14:58:19 +00:00
return limitModel(x) as ModelType;
2023-04-03 17:05:33 +00:00
},
max_tokens(x: number) {
return limitNumber(x, 0, 32000, 2000);
},
presence_penalty(x: number) {
return limitNumber(x, -2, 2, 0);
},
temperature(x: number) {
return limitNumber(x, 0, 2, 1);
},
};
2023-03-09 17:01:40 +00:00
2023-03-12 19:21:48 +00:00
const DEFAULT_CONFIG: ChatConfig = {
2023-03-19 16:09:30 +00:00
historyMessageCount: 4,
2023-03-19 16:29:09 +00:00
compressMessageLengthThreshold: 1000,
2023-03-19 15:13:10 +00:00
sendBotMessages: true as boolean,
2023-03-12 19:21:48 +00:00
submitKey: SubmitKey.CtrlEnter as SubmitKey,
2023-03-19 15:13:10 +00:00
avatar: "1f603",
2023-03-28 06:37:44 +00:00
fontSize: 14,
2023-03-12 19:21:48 +00:00
theme: Theme.Auto as Theme,
tightBorder: false,
sendPreviewBubble: true,
2023-04-09 16:54:17 +00:00
sidebarWidth: 300,
2023-03-21 16:20:32 +00:00
2023-03-28 17:30:11 +00:00
disablePromptHint: false,
2023-03-21 16:20:32 +00:00
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
},
2023-03-12 19:21:48 +00:00
};
2023-03-19 16:09:30 +00:00
export interface ChatStat {
2023-03-10 18:25:33 +00:00
tokenCount: number;
wordCount: number;
charCount: number;
}
2023-03-19 16:09:30 +00:00
export interface ChatSession {
2023-03-11 17:14:07 +00:00
id: number;
2023-03-10 18:25:33 +00:00
topic: string;
sendMemory: boolean;
2023-03-10 18:25:33 +00:00
memoryPrompt: string;
context: Message[];
2023-03-10 18:25:33 +00:00
messages: Message[];
stat: ChatStat;
lastUpdate: string;
2023-03-19 15:13:10 +00:00
lastSummarizeIndex: number;
2023-03-10 18:25:33 +00:00
}
2023-03-20 16:17:45 +00:00
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
2023-04-05 19:19:33 +00:00
export const BOT_HELLO: Message = createMessage({
2023-04-02 13:56:34 +00:00
role: "assistant",
content: Locale.Store.BotHello,
2023-04-05 19:19:33 +00:00
});
2023-03-10 18:25:33 +00:00
function createEmptySession(): ChatSession {
const createDate = new Date().toLocaleString();
return {
2023-03-11 17:14:07 +00:00
id: Date.now(),
2023-03-10 18:25:33 +00:00
topic: DEFAULT_TOPIC,
sendMemory: true,
2023-03-10 18:25:33 +00:00
memoryPrompt: "",
context: [],
2023-04-02 13:56:34 +00:00
messages: [],
2023-03-10 18:25:33 +00:00
stat: {
tokenCount: 0,
wordCount: 0,
charCount: 0,
},
lastUpdate: createDate,
2023-03-19 15:13:10 +00:00
lastSummarizeIndex: 0,
2023-03-10 18:25:33 +00:00
};
2023-03-09 17:01:40 +00:00
}
2023-03-10 18:25:33 +00:00
interface ChatStore {
2023-03-11 17:14:07 +00:00
config: ChatConfig;
2023-03-10 18:25:33 +00:00
sessions: ChatSession[];
currentSessionIndex: number;
2023-04-02 05:42:47 +00:00
clearSessions: () => void;
2023-03-10 18:25:33 +00:00
removeSession: (index: number) => void;
2023-04-05 17:34:46 +00:00
moveSession: (from: number, to: number) => void;
2023-03-10 18:25:33 +00:00
selectSession: (index: number) => void;
newSession: () => void;
2023-04-09 15:41:16 +00:00
deleteSession: (index?: number) => void;
2023-03-10 18:25:33 +00:00
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>;
summarizeSession: () => void;
updateStat: (message: Message) => void;
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
2023-03-11 12:54:24 +00:00
updateMessage: (
sessionIndex: number,
messageIndex: number,
2023-04-18 03:44:15 +00:00
updater: (message?: Message) => void,
2023-03-11 12:54:24 +00:00
) => void;
resetSession: () => void;
2023-03-19 15:13:10 +00:00
getMessagesWithMemory: () => Message[];
2023-03-21 16:20:32 +00:00
getMemoryPrompt: () => Message;
2023-03-11 17:14:07 +00:00
getConfig: () => ChatConfig;
2023-03-12 19:21:48 +00:00
resetConfig: () => void;
2023-03-11 17:14:07 +00:00
updateConfig: (updater: (config: ChatConfig) => void) => void;
2023-03-19 15:13:10 +00:00
clearAllData: () => void;
2023-03-09 17:01:40 +00:00
}
2023-03-29 16:02:50 +00:00
function countMessages(msgs: Message[]) {
return msgs.reduce((pre, cur) => pre + cur.content.length, 0);
}
2023-03-13 16:25:07 +00:00
const LOCAL_KEY = "chat-next-web-store";
2023-03-10 18:25:33 +00:00
export const useChatStore = create<ChatStore>()(
persist(
(set, get) => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
2023-03-11 17:14:07 +00:00
config: {
2023-03-12 19:21:48 +00:00
...DEFAULT_CONFIG,
},
clearSessions() {
2023-04-02 05:42:47 +00:00
set(() => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
}));
},
2023-03-12 19:21:48 +00:00
resetConfig() {
set(() => ({ config: { ...DEFAULT_CONFIG } }));
2023-03-11 17:14:07 +00:00
},
getConfig() {
return get().config;
},
updateConfig(updater) {
const config = get().config;
updater(config);
set(() => ({ config }));
},
2023-03-10 18:25:33 +00:00
selectSession(index: number) {
set({
currentSessionIndex: index,
});
},
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,
};
});
},
2023-04-05 17:34:46 +00:00
moveSession(from: number, to: number) {
set((state) => {
const { sessions, currentSessionIndex: oldIndex } = state;
// move the session
const newSessions = [...sessions];
const session = newSessions[from];
newSessions.splice(from, 1);
newSessions.splice(to, 0, session);
// modify current session id
let newIndex = oldIndex === from ? to : oldIndex;
if (oldIndex > from && oldIndex <= to) {
newIndex -= 1;
} else if (oldIndex < from && oldIndex >= to) {
newIndex += 1;
}
return {
currentSessionIndex: newIndex,
sessions: newSessions,
};
});
},
2023-03-10 18:25:33 +00:00
newSession() {
set((state) => ({
2023-03-11 08:24:17 +00:00
currentSessionIndex: 0,
sessions: [createEmptySession()].concat(state.sessions),
2023-03-10 18:25:33 +00:00
}));
},
2023-04-09 15:41:16 +00:00
deleteSession(i?: number) {
2023-04-06 16:14:27 +00:00
const deletedSession = get().currentSession();
2023-04-09 15:41:16 +00:00
const index = i ?? get().currentSessionIndex;
2023-04-06 16:14:27 +00:00
const isLastSession = get().sessions.length === 1;
if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
get().removeSession(index);
2023-04-09 15:41:16 +00:00
2023-04-09 15:51:12 +00:00
showToast(
Locale.Home.DeleteToast,
{
text: Locale.Home.Revert,
onClick() {
set((state) => ({
sessions: state.sessions
.slice(0, index)
.concat([deletedSession])
.concat(
2023-04-18 03:44:15 +00:00
state.sessions.slice(index + Number(isLastSession)),
2023-04-09 15:51:12 +00:00
),
}));
},
},
2023-04-18 03:44:15 +00:00
5000,
2023-04-09 15:51:12 +00:00
);
2023-04-06 16:14:27 +00:00
}
},
2023-03-10 18:25:33 +00:00
currentSession() {
let index = get().currentSessionIndex;
const sessions = get().sessions;
if (index < 0 || index >= sessions.length) {
index = Math.min(sessions.length - 1, Math.max(0, index));
set(() => ({ currentSessionIndex: index }));
}
2023-03-11 17:14:07 +00:00
const session = sessions[index];
return session;
2023-03-10 18:25:33 +00:00
},
onNewMessage(message) {
2023-03-21 16:20:32 +00:00
get().updateCurrentSession((session) => {
session.lastUpdate = new Date().toLocaleString();
});
2023-03-10 18:25:33 +00:00
get().updateStat(message);
get().summarizeSession();
},
async onUserInput(content) {
2023-04-05 19:19:33 +00:00
const userMessage: Message = createMessage({
2023-03-10 18:25:33 +00:00
role: "user",
content,
2023-04-05 19:19:33 +00:00
});
2023-03-10 18:25:33 +00:00
2023-04-05 19:19:33 +00:00
const botMessage: Message = createMessage({
2023-03-11 12:54:24 +00:00
role: "assistant",
streaming: true,
id: userMessage.id! + 1,
model: get().config.modelConfig.model,
2023-04-05 19:19:33 +00:00
});
2023-03-11 12:54:24 +00:00
2023-03-21 16:20:32 +00:00
// get recent messages
const recentMessages = get().getMessagesWithMemory();
const sendMessages = recentMessages.concat(userMessage);
2023-03-26 10:59:09 +00:00
const sessionIndex = get().currentSessionIndex;
const messageIndex = get().currentSession().messages.length + 1;
2023-03-19 16:09:30 +00:00
// save user's and bot's message
2023-03-11 12:54:24 +00:00
get().updateCurrentSession((session) => {
2023-03-19 16:09:30 +00:00
session.messages.push(userMessage);
2023-03-11 12:54:24 +00:00
session.messages.push(botMessage);
});
2023-03-26 10:59:09 +00:00
// make request
2023-03-21 16:20:32 +00:00
console.log("[User Input] ", sendMessages);
2023-03-19 16:09:30 +00:00
requestChatStream(sendMessages, {
2023-03-11 12:54:24 +00:00
onMessage(content, done) {
2023-03-26 10:59:09 +00:00
// stream response
2023-03-11 12:54:24 +00:00
if (done) {
2023-03-11 17:14:07 +00:00
botMessage.streaming = false;
botMessage.content = content;
2023-03-21 16:20:32 +00:00
get().onNewMessage(botMessage);
2023-04-05 19:19:33 +00:00
ControllerPool.remove(
sessionIndex,
2023-04-18 03:44:15 +00:00
botMessage.id ?? messageIndex,
2023-04-05 19:19:33 +00:00
);
2023-03-11 12:54:24 +00:00
} else {
botMessage.content = content;
set(() => ({}));
}
},
onError(error, statusCode) {
if (statusCode === 401) {
botMessage.content = Locale.Error.Unauthorized;
2023-04-16 10:07:43 +00:00
} else if (!error.message.includes("aborted")) {
botMessage.content += "\n\n" + Locale.Store.Error;
}
2023-03-11 17:14:07 +00:00
botMessage.streaming = false;
userMessage.isError = true;
botMessage.isError = true;
2023-03-11 17:14:07 +00:00
set(() => ({}));
2023-04-05 19:19:33 +00:00
ControllerPool.remove(sessionIndex, botMessage.id ?? messageIndex);
2023-03-26 10:59:09 +00:00
},
onController(controller) {
// collect controller for stop/retry
ControllerPool.addController(
sessionIndex,
2023-04-05 19:19:33 +00:00
botMessage.id ?? messageIndex,
2023-04-18 03:44:15 +00:00
controller,
2023-03-26 10:59:09 +00:00
);
2023-03-11 17:14:07 +00:00
},
filterBot: !get().config.sendBotMessages,
2023-03-21 16:20:32 +00:00
modelConfig: get().config.modelConfig,
2023-03-10 18:25:33 +00:00
});
},
2023-03-19 16:29:09 +00:00
getMemoryPrompt() {
2023-03-21 16:20:32 +00:00
const session = get().currentSession();
2023-03-19 16:29:09 +00:00
return {
2023-03-21 16:20:32 +00:00
role: "system",
2023-03-20 16:17:45 +00:00
content: Locale.Store.Prompt.History(session.memoryPrompt),
2023-03-21 16:20:32 +00:00
date: "",
} as Message;
2023-03-19 16:29:09 +00:00
},
2023-03-19 15:13:10 +00:00
getMessagesWithMemory() {
2023-03-21 16:20:32 +00:00
const session = get().currentSession();
const config = get().config;
const messages = session.messages.filter((msg) => !msg.isError);
const n = messages.length;
2023-03-19 15:13:10 +00:00
const context = session.context.slice();
2023-03-19 15:13:10 +00:00
// long term memory
if (
session.sendMemory &&
session.memoryPrompt &&
session.memoryPrompt.length > 0
) {
const memoryPrompt = get().getMemoryPrompt();
context.push(memoryPrompt);
2023-03-19 15:13:10 +00:00
}
// get short term and unmemoried long term memory
const shortTermMemoryMessageIndex = Math.max(
0,
2023-04-18 03:44:15 +00:00
n - config.historyMessageCount,
);
2023-04-18 03:44:15 +00:00
const longTermMemoryMessageIndex = session.lastSummarizeIndex;
const oldestIndex = Math.max(
shortTermMemoryMessageIndex,
2023-04-18 03:44:15 +00:00
longTermMemoryMessageIndex,
);
const threshold = config.compressMessageLengthThreshold;
// get recent messages as many as possible
const reversedRecentMessages = [];
for (
let i = n - 1, count = 0;
i >= oldestIndex && count < threshold;
i -= 1
) {
const msg = messages[i];
if (!msg || msg.isError) continue;
count += msg.content.length;
reversedRecentMessages.push(msg);
}
// concat
const recentMessages = context.concat(reversedRecentMessages.reverse());
2023-03-21 16:20:32 +00:00
return recentMessages;
2023-03-19 15:13:10 +00:00
},
2023-03-11 12:54:24 +00:00
updateMessage(
sessionIndex: number,
messageIndex: number,
2023-04-18 03:44:15 +00:00
updater: (message?: Message) => void,
2023-03-11 12:54:24 +00:00
) {
const sessions = get().sessions;
const session = sessions.at(sessionIndex);
const messages = session?.messages;
updater(messages?.at(messageIndex));
set(() => ({ sessions }));
},
resetSession() {
get().updateCurrentSession((session) => {
session.messages = [];
session.memoryPrompt = "";
});
},
2023-03-10 18:25:33 +00:00
summarizeSession() {
const session = get().currentSession();
2023-03-29 16:02:50 +00:00
// should summarize topic after chating more than 50 words
const SUMMARIZE_MIN_LEN = 50;
if (
session.topic === DEFAULT_TOPIC &&
countMessages(session.messages) >= SUMMARIZE_MIN_LEN
) {
2023-03-21 16:20:32 +00:00
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
(res) => {
get().updateCurrentSession(
2023-04-05 19:19:33 +00:00
(session) =>
2023-04-18 03:44:15 +00:00
(session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
2023-03-21 16:20:32 +00:00
);
2023-04-18 03:44:15 +00:00
},
2023-03-21 16:20:32 +00:00
);
2023-03-13 16:25:07 +00:00
}
2023-03-19 15:13:10 +00:00
2023-03-21 16:20:32 +00:00
const config = get().config;
let toBeSummarizedMsgs = session.messages.slice(
2023-04-18 03:44:15 +00:00
session.lastSummarizeIndex,
2023-03-21 16:20:32 +00:00
);
2023-03-29 16:02:50 +00:00
const historyMsgLength = countMessages(toBeSummarizedMsgs);
2023-03-19 16:29:09 +00:00
if (historyMsgLength > get().config?.modelConfig?.max_tokens ?? 4000) {
const n = toBeSummarizedMsgs.length;
2023-03-21 16:20:32 +00:00
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
2023-04-18 03:44:15 +00:00
Math.max(0, n - config.historyMessageCount),
2023-03-21 16:20:32 +00:00
);
2023-03-19 16:29:09 +00:00
}
// add memory prompt
2023-03-21 16:20:32 +00:00
toBeSummarizedMsgs.unshift(get().getMemoryPrompt());
2023-03-19 16:29:09 +00:00
2023-03-21 16:20:32 +00:00
const lastSummarizeIndex = session.messages.length;
2023-03-19 16:09:30 +00:00
2023-03-21 16:20:32 +00:00
console.log(
"[Chat History] ",
toBeSummarizedMsgs,
historyMsgLength,
2023-04-18 03:44:15 +00:00
config.compressMessageLengthThreshold,
2023-03-21 16:20:32 +00:00
);
2023-03-19 16:09:30 +00:00
if (
historyMsgLength > config.compressMessageLengthThreshold &&
session.sendMemory
) {
2023-03-21 16:20:32 +00:00
requestChatStream(
toBeSummarizedMsgs.concat({
role: "system",
content: Locale.Store.Prompt.Summarize,
date: "",
}),
{
filterBot: false,
onMessage(message, done) {
session.memoryPrompt = message;
if (done) {
console.log("[Memory] ", session.memoryPrompt);
session.lastSummarizeIndex = lastSummarizeIndex;
}
},
onError(error) {
console.error("[Summarize] ", error);
},
2023-04-18 03:44:15 +00:00
},
2023-03-21 16:20:32 +00:00
);
2023-03-19 15:13:10 +00:00
}
2023-03-10 18:25:33 +00:00
},
updateStat(message) {
get().updateCurrentSession((session) => {
session.stat.charCount += message.content.length;
// TODO: should update chat count and word count
});
},
updateCurrentSession(updater) {
const sessions = get().sessions;
const index = get().currentSessionIndex;
updater(sessions[index]);
set(() => ({ sessions }));
},
2023-03-19 15:13:10 +00:00
clearAllData() {
2023-03-20 16:17:45 +00:00
if (confirm(Locale.Store.ConfirmClearAll)) {
2023-03-21 16:20:32 +00:00
localStorage.clear();
location.reload();
2023-03-19 15:13:10 +00:00
}
},
2023-03-10 18:25:33 +00:00
}),
2023-03-13 16:25:07 +00:00
{
name: LOCAL_KEY,
version: 1.2,
migrate(persistedState, version) {
const state = persistedState as ChatStore;
if (version === 1) {
state.sessions.forEach((s) => (s.context = []));
}
if (version < 1.2) {
state.sessions.forEach((s) => (s.sendMemory = true));
}
return state;
},
2023-04-18 03:44:15 +00:00
},
),
2023-03-10 18:25:33 +00:00
);