Merge branch 'Yidadaa:main' into main

This commit is contained in:
Wei Xia 2023-04-23 14:44:11 +08:00 committed by GitHub
commit d09550b4cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 239 additions and 213 deletions

View File

@ -102,7 +102,7 @@ We recommend that you follow the steps below to re-deploy:
### Enable Automatic Updates ### Enable Automatic Updates
After forking the project, due to the limitations imposed by Github, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour: After forking the project, due to the limitations imposed by GitHub, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
![Automatic Updates](./docs/images/enable-actions.jpg) ![Automatic Updates](./docs/images/enable-actions.jpg)
@ -110,7 +110,7 @@ After forking the project, due to the limitations imposed by Github, you need to
### Manually Updating Code ### Manually Updating Code
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code. If you want to update instantly, you can check out the [GitHub documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
You can star or watch this project or follow author to get release notifictions in time. You can star or watch this project or follow author to get release notifictions in time.

View File

@ -32,6 +32,7 @@ import {
useAccessStore, useAccessStore,
Theme, Theme,
ModelType, ModelType,
useAppConfig,
} from "../store"; } from "../store";
import { import {
@ -69,7 +70,7 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
}); });
export function Avatar(props: { role: Message["role"]; model?: ModelType }) { export function Avatar(props: { role: Message["role"]; model?: ModelType }) {
const config = useChatStore((state) => state.config); const config = useAppConfig();
if (props.role !== "user") { if (props.role !== "user") {
return ( return (
@ -285,7 +286,7 @@ function PromptToast(props: {
} }
function useSubmitHandler() { function useSubmitHandler() {
const config = useChatStore((state) => state.config); const config = useAppConfig();
const submitKey = config.submitKey; const submitKey = config.submitKey;
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@ -361,16 +362,16 @@ export function ChatActions(props: {
scrollToBottom: () => void; scrollToBottom: () => void;
hitBottom: boolean; hitBottom: boolean;
}) { }) {
const chatStore = useChatStore(); const config = useAppConfig();
// switch themes // switch themes
const theme = chatStore.config.theme; const theme = config.theme;
function nextTheme() { function nextTheme() {
const themes = [Theme.Auto, Theme.Light, Theme.Dark]; const themes = [Theme.Auto, Theme.Light, Theme.Dark];
const themeIndex = themes.indexOf(theme); const themeIndex = themes.indexOf(theme);
const nextIndex = (themeIndex + 1) % themes.length; const nextIndex = (themeIndex + 1) % themes.length;
const nextTheme = themes[nextIndex]; const nextTheme = themes[nextIndex];
chatStore.updateConfig((config) => (config.theme = nextTheme)); config.update((config) => (config.theme = nextTheme));
} }
// stop all responses // stop all responses
@ -428,7 +429,8 @@ export function Chat() {
state.currentSession(), state.currentSession(),
state.currentSessionIndex, state.currentSessionIndex,
]); ]);
const fontSize = useChatStore((state) => state.config.fontSize); const config = useAppConfig();
const fontSize = config.fontSize;
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState(""); const [userInput, setUserInput] = useState("");
@ -492,7 +494,7 @@ export function Chat() {
// clear search results // clear search results
if (n === 0) { if (n === 0) {
setPromptHints([]); setPromptHints([]);
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) { } else if (!config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
// check if need to trigger auto completion // check if need to trigger auto completion
if (text.startsWith("/")) { if (text.startsWith("/")) {
let searchText = text.slice(1); let searchText = text.slice(1);
@ -543,7 +545,7 @@ export function Chat() {
} }
}; };
const findLastUesrIndex = (messageId: number) => { const findLastUserIndex = (messageId: number) => {
// find last user input message and resend // find last user input message and resend
let lastUserMessageIndex: number | null = null; let lastUserMessageIndex: number | null = null;
for (let i = 0; i < session.messages.length; i += 1) { for (let i = 0; i < session.messages.length; i += 1) {
@ -566,14 +568,14 @@ export function Chat() {
}; };
const onDelete = (botMessageId: number) => { const onDelete = (botMessageId: number) => {
const userIndex = findLastUesrIndex(botMessageId); const userIndex = findLastUserIndex(botMessageId);
if (userIndex === null) return; if (userIndex === null) return;
deleteMessage(userIndex); deleteMessage(userIndex);
}; };
const onResend = (botMessageId: number) => { const onResend = (botMessageId: number) => {
// find last user input message and resend // find last user input message and resend
const userIndex = findLastUesrIndex(botMessageId); const userIndex = findLastUserIndex(botMessageId);
if (userIndex === null) return; if (userIndex === null) return;
setIsLoading(true); setIsLoading(true);
@ -583,8 +585,6 @@ export function Chat() {
inputRef.current?.focus(); inputRef.current?.focus();
}; };
const config = useChatStore((state) => state.config);
const context: RenderMessage[] = session.context.slice(); const context: RenderMessage[] = session.context.slice();
const accessStore = useAccessStore(); const accessStore = useAccessStore();
@ -692,10 +692,10 @@ export function Chat() {
{!isMobileScreen && ( {!isMobileScreen && (
<div className={styles["window-action-button"]}> <div className={styles["window-action-button"]}>
<IconButton <IconButton
icon={chatStore.config.tightBorder ? <MinIcon /> : <MaxIcon />} icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
bordered bordered
onClick={() => { onClick={() => {
chatStore.updateConfig( config.update(
(config) => (config.tightBorder = !config.tightBorder), (config) => (config.tightBorder = !config.tightBorder),
); );
}} }}

View File

@ -313,6 +313,10 @@
.chat-message { .chat-message {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
&:last-child {
animation: slide-in ease 0.3s;
}
} }
.chat-message-user { .chat-message-user {
@ -325,7 +329,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
animation: slide-in ease 0.3s;
&:hover { &:hover {
.chat-message-top-actions { .chat-message-top-actions {

View File

@ -2,14 +2,13 @@
require("../polyfill"); require("../polyfill");
import { useState, useEffect } from "react"; import { useState, useEffect, StyleHTMLAttributes } from "react";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
import BotIcon from "../icons/bot.svg"; import BotIcon from "../icons/bot.svg";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
import { useChatStore } from "../store";
import { getCSSVar, useMobileScreen } from "../utils"; import { getCSSVar, useMobileScreen } from "../utils";
import { Chat } from "./chat"; import { Chat } from "./chat";
@ -23,6 +22,8 @@ import {
Route, Route,
useLocation, useLocation,
} from "react-router-dom"; } from "react-router-dom";
import { SideBar } from "./sidebar";
import { useAppConfig } from "../store/config";
export function Loading(props: { noLogo?: boolean }) { export function Loading(props: { noLogo?: boolean }) {
return ( return (
@ -37,12 +38,8 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
loading: () => <Loading noLogo />, loading: () => <Loading noLogo />,
}); });
const SideBar = dynamic(async () => (await import("./sidebar")).SideBar, {
loading: () => <Loading noLogo />,
});
export function useSwitchTheme() { export function useSwitchTheme() {
const config = useChatStore((state) => state.config); const config = useAppConfig();
useEffect(() => { useEffect(() => {
document.body.classList.remove("light"); document.body.classList.remove("light");
@ -83,7 +80,7 @@ const useHasHydrated = () => {
}; };
function WideScreen() { function WideScreen() {
const config = useChatStore((state) => state.config); const config = useAppConfig();
return ( return (
<div <div

View File

@ -23,6 +23,7 @@ import {
useUpdateStore, useUpdateStore,
useAccessStore, useAccessStore,
ModalConfigValidator, ModalConfigValidator,
useAppConfig,
} from "../store"; } from "../store";
import { Avatar } from "./chat"; import { Avatar } from "./chat";
@ -180,14 +181,13 @@ function PasswordInput(props: HTMLProps<HTMLInputElement>) {
export function Settings() { export function Settings() {
const navigate = useNavigate(); const navigate = useNavigate();
const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [config, updateConfig, resetConfig, clearAllData, clearSessions] = const config = useAppConfig();
useChatStore((state) => [ const updateConfig = config.update;
state.config, const resetConfig = config.reset;
state.updateConfig, const [clearAllData, clearSessions] = useChatStore((state) => [
state.resetConfig, state.clearAllData,
state.clearAllData, state.clearSessions,
state.clearSessions, ]);
]);
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
const [checkingUpdate, setCheckingUpdate] = useState(false); const [checkingUpdate, setCheckingUpdate] = useState(false);
@ -645,7 +645,7 @@ export function Settings() {
value={config.modelConfig.presence_penalty?.toFixed(1)} value={config.modelConfig.presence_penalty?.toFixed(1)}
min="-2" min="-2"
max="2" max="2"
step="0.5" step="0.1"
onChange={(e) => { onChange={(e) => {
updateConfig( updateConfig(
(config) => (config) =>

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
@ -10,7 +10,7 @@ import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg"; import CloseIcon from "../icons/close.svg";
import Locale from "../locales"; import Locale from "../locales";
import { useChatStore } from "../store"; import { useAppConfig, useChatStore } from "../store";
import { import {
MAX_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH,
@ -20,16 +20,20 @@ import {
REPO_URL, REPO_URL,
} from "../constant"; } from "../constant";
import { HashRouter as Router, Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useMobileScreen } from "../utils"; import { useMobileScreen } from "../utils";
import { ChatList } from "./chat-list"; import dynamic from "next/dynamic";
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
loading: () => null,
});
function useDragSideBar() { function useDragSideBar() {
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x); const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
const chatStore = useChatStore(); const config = useAppConfig();
const startX = useRef(0); const startX = useRef(0);
const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300); const startDragWidth = useRef(config.sidebarWidth ?? 300);
const lastUpdateTime = useRef(Date.now()); const lastUpdateTime = useRef(Date.now());
const handleMouseMove = useRef((e: MouseEvent) => { const handleMouseMove = useRef((e: MouseEvent) => {
@ -39,11 +43,11 @@ function useDragSideBar() {
lastUpdateTime.current = Date.now(); lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current; const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d); const nextWidth = limit(startDragWidth.current + d);
chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth)); config.update((config) => (config.sidebarWidth = nextWidth));
}); });
const handleMouseUp = useRef(() => { const handleMouseUp = useRef(() => {
startDragWidth.current = chatStore.config.sidebarWidth ?? 300; startDragWidth.current = config.sidebarWidth ?? 300;
window.removeEventListener("mousemove", handleMouseMove.current); window.removeEventListener("mousemove", handleMouseMove.current);
window.removeEventListener("mouseup", handleMouseUp.current); window.removeEventListener("mouseup", handleMouseUp.current);
}); });
@ -56,15 +60,15 @@ function useDragSideBar() {
}; };
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const shouldNarrow = const shouldNarrow =
!isMobileScreen && chatStore.config.sidebarWidth < MIN_SIDEBAR_WIDTH; !isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
useEffect(() => { useEffect(() => {
const barWidth = shouldNarrow const barWidth = shouldNarrow
? NARROW_SIDEBAR_WIDTH ? NARROW_SIDEBAR_WIDTH
: limit(chatStore.config.sidebarWidth ?? 300); : limit(config.sidebarWidth ?? 300);
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`; const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth); document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
}, [chatStore.config.sidebarWidth, isMobileScreen, shouldNarrow]); }, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
return { return {
onDragMouseDown, onDragMouseDown,

View File

@ -2,7 +2,7 @@ import styles from "./ui-lib.module.scss";
import LoadingIcon from "../icons/three-dots.svg"; import LoadingIcon from "../icons/three-dots.svg";
import CloseIcon from "../icons/close.svg"; import CloseIcon from "../icons/close.svg";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import React from "react"; import React, { useEffect } from "react";
export function Popover(props: { export function Popover(props: {
children: JSX.Element; children: JSX.Element;
@ -64,6 +64,21 @@ interface ModalProps {
onClose?: () => void; onClose?: () => void;
} }
export function Modal(props: ModalProps) { export function Modal(props: ModalProps) {
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
props.onClose?.();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return ( return (
<div className={styles["modal-container"]}> <div className={styles["modal-container"]}>
<div className={styles["modal-header"]}> <div className={styles["modal-header"]}>

View File

@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app"; import { SubmitKey } from "../store/config";
const cn = { const cn = {
WIP: "该功能仍在开发中……", WIP: "该功能仍在开发中……",

View File

@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { LocaleType } from "./index";
const de: LocaleType = { const de: LocaleType = {

View File

@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { LocaleType } from "./index";
const en: LocaleType = { const en: LocaleType = {

View File

@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { LocaleType } from "./index";
const es: LocaleType = { const es: LocaleType = {

View File

@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { LocaleType } from "./index";
const it: LocaleType = { const it: LocaleType = {

View File

@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app"; import { SubmitKey } from "../store/config";
const jp = { const jp = {
WIP: "この機能は開発中です……", WIP: "この機能は開発中です……",

View File

@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { LocaleType } from "./index";
const tr: LocaleType = { const tr: LocaleType = {

View File

@ -1,4 +1,4 @@
import { SubmitKey } from "../store/app"; import { SubmitKey } from "../store/config";
import type { LocaleType } from "./index"; import type { LocaleType } from "./index";
const tw: LocaleType = { const tw: LocaleType = {

View File

@ -4,6 +4,7 @@ import {
ModelConfig, ModelConfig,
ModelType, ModelType,
useAccessStore, useAccessStore,
useAppConfig,
useChatStore, useChatStore,
} from "./store"; } from "./store";
import { showToast } from "./components/ui-lib"; import { showToast } from "./components/ui-lib";
@ -27,7 +28,7 @@ const makeRequestParam = (
sendMessages = sendMessages.filter((m) => m.role !== "assistant"); sendMessages = sendMessages.filter((m) => m.role !== "assistant");
} }
const modelConfig = { ...useChatStore.getState().config.modelConfig }; const modelConfig = { ...useAppConfig.getState().modelConfig };
// @yidadaa: wont send max_tokens, because it is nonsense for Muggles // @yidadaa: wont send max_tokens, because it is nonsense for Muggles
// @ts-expect-error // @ts-expect-error
@ -149,6 +150,7 @@ export async function requestChatStream(
options?: { options?: {
filterBot?: boolean; filterBot?: boolean;
modelConfig?: ModelConfig; modelConfig?: ModelConfig;
model?: ModelType;
onMessage: (message: string, done: boolean) => void; onMessage: (message: string, done: boolean) => void;
onError: (error: Error, statusCode?: number) => void; onError: (error: Error, statusCode?: number) => void;
onController?: (controller: AbortController) => void; onController?: (controller: AbortController) => void;
@ -157,6 +159,7 @@ export async function requestChatStream(
const req = makeRequestParam(messages, { const req = makeRequestParam(messages, {
stream: true, stream: true,
filterBot: options?.filterBot, filterBot: options?.filterBot,
model: options?.model,
}); });
console.log("[Request] ", req); console.log("[Request] ", req);

View File

@ -11,6 +11,7 @@ import { isMobileScreen, trimTopic } from "../utils";
import Locale from "../locales"; import Locale from "../locales";
import { showToast } from "../components/ui-lib"; import { showToast } from "../components/ui-lib";
import { ModelType, useAppConfig } from "./config";
export type Message = ChatCompletionResponseMessage & { export type Message = ChatCompletionResponseMessage & {
date: string; date: string;
@ -30,133 +31,8 @@ export function createMessage(override: Partial<Message>): Message {
}; };
} }
export enum SubmitKey {
Enter = "Enter",
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
MetaEnter = "Meta + Enter",
}
export enum Theme {
Auto = "auto",
Dark = "dark",
Light = "light",
}
export interface ChatConfig {
historyMessageCount: number; // -1 means all
compressMessageLengthThreshold: number;
sendBotMessages: boolean; // send bot's message or not
submitKey: SubmitKey;
avatar: string;
fontSize: number;
theme: Theme;
tightBorder: boolean;
sendPreviewBubble: boolean;
sidebarWidth: number;
disablePromptHint: boolean;
modelConfig: {
model: ModelType;
temperature: number;
max_tokens: number;
presence_penalty: number;
};
}
export type ModelConfig = ChatConfig["modelConfig"];
export const ROLES: Message["role"][] = ["system", "user", "assistant"]; export const ROLES: Message["role"][] = ["system", "user", "assistant"];
const ENABLE_GPT4 = true;
export const ALL_MODELS = [
{
name: "gpt-4",
available: ENABLE_GPT4,
},
{
name: "gpt-4-0314",
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k",
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k-0314",
available: ENABLE_GPT4,
},
{
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"];
export function limitNumber(
x: number,
min: number,
max: number,
defaultValue: number,
) {
if (typeof x !== "number" || isNaN(x)) {
return defaultValue;
}
return Math.min(max, Math.max(min, x));
}
export function limitModel(name: string) {
return ALL_MODELS.some((m) => m.name === name && m.available)
? name
: ALL_MODELS[4].name;
}
export const ModalConfigValidator = {
model(x: string) {
return limitModel(x) as ModelType;
},
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);
},
};
const DEFAULT_CONFIG: ChatConfig = {
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
sendBotMessages: true as boolean,
submitKey: SubmitKey.CtrlEnter as SubmitKey,
avatar: "1f603",
fontSize: 14,
theme: Theme.Auto as Theme,
tightBorder: false,
sendPreviewBubble: true,
sidebarWidth: 300,
disablePromptHint: false,
modelConfig: {
model: "gpt-3.5-turbo",
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
},
};
export interface ChatStat { export interface ChatStat {
tokenCount: number; tokenCount: number;
wordCount: number; wordCount: number;
@ -202,7 +78,6 @@ function createEmptySession(): ChatSession {
} }
interface ChatStore { interface ChatStore {
config: ChatConfig;
sessions: ChatSession[]; sessions: ChatSession[];
currentSessionIndex: number; currentSessionIndex: number;
clearSessions: () => void; clearSessions: () => void;
@ -226,9 +101,6 @@ interface ChatStore {
getMessagesWithMemory: () => Message[]; getMessagesWithMemory: () => Message[];
getMemoryPrompt: () => Message; getMemoryPrompt: () => Message;
getConfig: () => ChatConfig;
resetConfig: () => void;
updateConfig: (updater: (config: ChatConfig) => void) => void;
clearAllData: () => void; clearAllData: () => void;
} }
@ -243,9 +115,6 @@ export const useChatStore = create<ChatStore>()(
(set, get) => ({ (set, get) => ({
sessions: [createEmptySession()], sessions: [createEmptySession()],
currentSessionIndex: 0, currentSessionIndex: 0,
config: {
...DEFAULT_CONFIG,
},
clearSessions() { clearSessions() {
set(() => ({ set(() => ({
@ -254,20 +123,6 @@ export const useChatStore = create<ChatStore>()(
})); }));
}, },
resetConfig() {
set(() => ({ config: { ...DEFAULT_CONFIG } }));
},
getConfig() {
return get().config;
},
updateConfig(updater) {
const config = get().config;
updater(config);
set(() => ({ config }));
},
selectSession(index: number) { selectSession(index: number) {
set({ set({
currentSessionIndex: index, currentSessionIndex: index,
@ -390,7 +245,7 @@ export const useChatStore = create<ChatStore>()(
role: "assistant", role: "assistant",
streaming: true, streaming: true,
id: userMessage.id! + 1, id: userMessage.id! + 1,
model: get().config.modelConfig.model, model: useAppConfig.getState().modelConfig.model,
}); });
// get recent messages // get recent messages
@ -443,8 +298,8 @@ export const useChatStore = create<ChatStore>()(
controller, controller,
); );
}, },
filterBot: !get().config.sendBotMessages, filterBot: !useAppConfig.getState().sendBotMessages,
modelConfig: get().config.modelConfig, modelConfig: useAppConfig.getState().modelConfig,
}); });
}, },
@ -460,7 +315,7 @@ export const useChatStore = create<ChatStore>()(
getMessagesWithMemory() { getMessagesWithMemory() {
const session = get().currentSession(); const session = get().currentSession();
const config = get().config; const config = useAppConfig.getState();
const messages = session.messages.filter((msg) => !msg.isError); const messages = session.messages.filter((msg) => !msg.isError);
const n = messages.length; const n = messages.length;
@ -545,14 +400,14 @@ export const useChatStore = create<ChatStore>()(
}); });
} }
const config = get().config; const config = useAppConfig.getState();
let toBeSummarizedMsgs = session.messages.slice( let toBeSummarizedMsgs = session.messages.slice(
session.lastSummarizeIndex, session.lastSummarizeIndex,
); );
const historyMsgLength = countMessages(toBeSummarizedMsgs); const historyMsgLength = countMessages(toBeSummarizedMsgs);
if (historyMsgLength > get().config?.modelConfig?.max_tokens ?? 4000) { if (historyMsgLength > config?.modelConfig?.max_tokens ?? 4000) {
const n = toBeSummarizedMsgs.length; const n = toBeSummarizedMsgs.length;
toBeSummarizedMsgs = toBeSummarizedMsgs.slice( toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
Math.max(0, n - config.historyMessageCount), Math.max(0, n - config.historyMessageCount),
@ -583,6 +438,7 @@ export const useChatStore = create<ChatStore>()(
}), }),
{ {
filterBot: false, filterBot: false,
model: "gpt-3.5-turbo",
onMessage(message, done) { onMessage(message, done) {
session.memoryPrompt = message; session.memoryPrompt = message;
if (done) { if (done) {

135
app/store/config.ts Normal file
View File

@ -0,0 +1,135 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
export enum SubmitKey {
Enter = "Enter",
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
MetaEnter = "Meta + Enter",
}
export enum Theme {
Auto = "auto",
Dark = "dark",
Light = "light",
}
const DEFAULT_CONFIG = {
historyMessageCount: 4,
compressMessageLengthThreshold: 1000,
sendBotMessages: true as boolean,
submitKey: SubmitKey.CtrlEnter as SubmitKey,
avatar: "1f603",
fontSize: 14,
theme: Theme.Auto as Theme,
tightBorder: false,
sendPreviewBubble: true,
sidebarWidth: 300,
disablePromptHint: false,
modelConfig: {
model: "gpt-3.5-turbo" as ModelType,
temperature: 1,
max_tokens: 2000,
presence_penalty: 0,
},
};
export type ChatConfig = typeof DEFAULT_CONFIG;
export type ChatConfigStore = ChatConfig & {
reset: () => void;
update: (updater: (config: ChatConfig) => void) => void;
};
export type ModelConfig = ChatConfig["modelConfig"];
const ENABLE_GPT4 = true;
export const ALL_MODELS = [
{
name: "gpt-4",
available: ENABLE_GPT4,
},
{
name: "gpt-4-0314",
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k",
available: ENABLE_GPT4,
},
{
name: "gpt-4-32k-0314",
available: ENABLE_GPT4,
},
{
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"];
export function limitNumber(
x: number,
min: number,
max: number,
defaultValue: number,
) {
if (typeof x !== "number" || isNaN(x)) {
return defaultValue;
}
return Math.min(max, Math.max(min, x));
}
export function limitModel(name: string) {
return ALL_MODELS.some((m) => m.name === name && m.available)
? name
: ALL_MODELS[4].name;
}
export const ModalConfigValidator = {
model(x: string) {
return limitModel(x) as ModelType;
},
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);
},
};
const CONFIG_KEY = "app-config";
export const useAppConfig = create<ChatConfigStore>()(
persist(
(set, get) => ({
...DEFAULT_CONFIG,
reset() {
set(() => ({ ...DEFAULT_CONFIG }));
},
update(updater) {
const config = { ...get() };
updater(config);
set(() => config);
},
}),
{
name: CONFIG_KEY,
},
),
);

View File

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

View File

@ -10,10 +10,20 @@ const RAW_EN_URL = "f/awesome-chatgpt-prompts/main/prompts.csv";
const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL; const EN_URL = MIRRORF_FILE_URL + RAW_EN_URL;
const FILE = "./public/prompts.json"; const FILE = "./public/prompts.json";
const timeoutPromise = (timeout) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Request timeout'));
}, timeout);
});
};
async function fetchCN() { async function fetchCN() {
console.log("[Fetch] fetching cn prompts..."); console.log("[Fetch] fetching cn prompts...");
try { try {
const raw = await (await fetch(CN_URL)).json(); // const raw = await (await fetch(CN_URL)).json();
const response = await Promise.race([fetch(CN_URL), timeoutPromise(5000)]);
const raw = await response.json();
return raw.map((v) => [v.act, v.prompt]); return raw.map((v) => [v.act, v.prompt]);
} catch (error) { } catch (error) {
console.error("[Fetch] failed to fetch cn prompts", error); console.error("[Fetch] failed to fetch cn prompts", error);
@ -24,13 +34,15 @@ async function fetchCN() {
async function fetchEN() { async function fetchEN() {
console.log("[Fetch] fetching en prompts..."); console.log("[Fetch] fetching en prompts...");
try { try {
const raw = await (await fetch(EN_URL)).text(); // const raw = await (await fetch(EN_URL)).text();
const response = await Promise.race([fetch(EN_URL), timeoutPromise(5000)]);
const raw = await response.text();
return raw return raw
.split("\n") .split("\n")
.slice(1) .slice(1)
.map((v) => v.split('","').map((v) => v.replace('"', ""))); .map((v) => v.split('","').map((v) => v.replace('"', "")));
} catch (error) { } catch (error) {
console.error("[Fetch] failed to fetch cn prompts", error); console.error("[Fetch] failed to fetch en prompts", error);
return []; return [];
} }
} }

View File

@ -29,13 +29,13 @@ esac
if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v yarn >/dev/null; then if ! command -v node >/dev/null || ! command -v git >/dev/null || ! command -v yarn >/dev/null; then
case "$(uname -s)" in case "$(uname -s)" in
Linux) Linux)
if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"ubuntu\"" ]]; then if [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=ubuntu" ]]; then
sudo apt-get update sudo apt-get update
sudo apt-get -y install nodejs git yarn sudo apt-get -y install nodejs git yarn
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"centos\"" ]]; then elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=centos" ]]; then
sudo yum -y install epel-release sudo yum -y install epel-release
sudo yum -y install nodejs git yarn sudo yum -y install nodejs git yarn
elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=\"arch\"" ]]; then elif [[ "$(cat /etc/*-release | grep '^ID=')" = "ID=arch" ]]; then
sudo pacman -Syu -y sudo pacman -Syu -y
sudo pacman -S -y nodejs git yarn sudo pacman -S -y nodejs git yarn
else else