Merge branch 'main' into feat-drop-upload

This commit is contained in:
DDMeaqua 2024-09-14 16:27:44 +08:00
commit 423fbff624
45 changed files with 547 additions and 108 deletions

View File

@ -13,7 +13,9 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
if (config.disableGPT4) { if (config.disableGPT4) {
remoteModelRes.data = remoteModelRes.data.filter( remoteModelRes.data = remoteModelRes.data.filter(
(m) => !m.id.startsWith("gpt-4") || m.id.startsWith("gpt-4o-mini"), (m) =>
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o")) ||
m.id.startsWith("gpt-4o-mini"),
); );
} }

View File

@ -203,7 +203,7 @@ export class ClaudeApi implements LLMApi {
const [tools, funcs] = usePluginStore const [tools, funcs] = usePluginStore
.getState() .getState()
.getAsTools( .getAsTools(
useChatStore.getState().currentSession().mask?.plugin as string[], useChatStore.getState().currentSession().mask?.plugin || [],
); );
return stream( return stream(
path, path,

View File

@ -125,7 +125,7 @@ export class MoonshotApi implements LLMApi {
const [tools, funcs] = usePluginStore const [tools, funcs] = usePluginStore
.getState() .getState()
.getAsTools( .getAsTools(
useChatStore.getState().currentSession().mask?.plugin as string[], useChatStore.getState().currentSession().mask?.plugin || [],
); );
return stream( return stream(
chatPath, chatPath,

View File

@ -160,6 +160,7 @@ export class ChatGPTApi implements LLMApi {
let requestPayload: RequestPayload | DalleRequestPayload; let requestPayload: RequestPayload | DalleRequestPayload;
const isDalle3 = _isDalle3(options.config.model); const isDalle3 = _isDalle3(options.config.model);
const isO1 = options.config.model.startsWith("o1");
if (isDalle3) { if (isDalle3) {
const prompt = getMessageTextContent( const prompt = getMessageTextContent(
options.messages.slice(-1)?.pop() as any, options.messages.slice(-1)?.pop() as any,
@ -181,30 +182,32 @@ export class ChatGPTApi implements LLMApi {
const content = visionModel const content = visionModel
? await preProcessImageContent(v.content) ? await preProcessImageContent(v.content)
: getMessageTextContent(v); : getMessageTextContent(v);
if (!(isO1 && v.role === "system"))
messages.push({ role: v.role, content }); messages.push({ role: v.role, content });
} }
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
requestPayload = { requestPayload = {
messages, messages,
stream: options.config.stream, stream: !isO1 ? options.config.stream : false,
model: modelConfig.model, model: modelConfig.model,
temperature: modelConfig.temperature, temperature: !isO1 ? modelConfig.temperature : 1,
presence_penalty: modelConfig.presence_penalty, presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
frequency_penalty: modelConfig.frequency_penalty, frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
top_p: modelConfig.top_p, top_p: !isO1 ? modelConfig.top_p : 1,
// max_tokens: Math.max(modelConfig.max_tokens, 1024), // max_tokens: Math.max(modelConfig.max_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
}; };
// add max_tokens to vision model // add max_tokens to vision model
if (visionModel && modelConfig.model.includes("preview")) { if (visionModel) {
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
} }
} }
console.log("[Request] openai payload: ", requestPayload); console.log("[Request] openai payload: ", requestPayload);
const shouldStream = !isDalle3 && !!options.config.stream; const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
const controller = new AbortController(); const controller = new AbortController();
options.onController?.(controller); options.onController?.(controller);
@ -244,7 +247,7 @@ export class ChatGPTApi implements LLMApi {
const [tools, funcs] = usePluginStore const [tools, funcs] = usePluginStore
.getState() .getState()
.getAsTools( .getAsTools(
useChatStore.getState().currentSession().mask?.plugin as string[], useChatStore.getState().currentSession().mask?.plugin || [],
); );
// console.log("getAsTools", tools, funcs); // console.log("getAsTools", tools, funcs);
stream( stream(
@ -313,7 +316,7 @@ export class ChatGPTApi implements LLMApi {
// make a fetch request // make a fetch request
const requestTimeoutId = setTimeout( const requestTimeoutId = setTimeout(
() => controller.abort(), () => controller.abort(),
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
); );
const res = await fetch(chatPath, chatPayload); const res = await fetch(chatPath, chatPayload);
@ -407,7 +410,9 @@ export class ChatGPTApi implements LLMApi {
}); });
const resJson = (await res.json()) as OpenAIListModelResponse; const resJson = (await res.json()) as OpenAIListModelResponse;
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-")); const chatModels = resJson.data?.filter(
(m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
);
console.log("[Models]", chatModels); console.log("[Models]", chatModels);
if (!chatModels) { if (!chatModels) {

View File

@ -80,7 +80,7 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
}, [props.autoHeight, props.height, iframeHeight]); }, [props.autoHeight, props.height, iframeHeight]);
const srcDoc = useMemo(() => { const srcDoc = useMemo(() => {
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`; const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
if (props.code.includes("<!DOCTYPE html>")) { if (props.code.includes("<!DOCTYPE html>")) {
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script); props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
} }

View File

@ -647,6 +647,53 @@
} }
} }
.shortcut-key-container {
padding: 10px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.shortcut-key-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 16px;
}
.shortcut-key-item {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
padding: 10px;
background-color: var(--white);
}
.shortcut-key-title {
font-size: 14px;
color: var(--black);
}
.shortcut-key-keys {
display: flex;
gap: 8px;
}
.shortcut-key {
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
border-radius: 8px;
padding: 4px;
background-color: var(--gray);
min-width: 32px;
}
.shortcut-key span {
font-size: 12px;
color: var(--black);
}
.drag-overlay { .drag-overlay {
display: none; display: none;
} }

View File

@ -42,6 +42,7 @@ import SizeIcon from "../icons/size.svg";
import QualityIcon from "../icons/hd.svg"; import QualityIcon from "../icons/hd.svg";
import StyleIcon from "../icons/palette.svg"; import StyleIcon from "../icons/palette.svg";
import PluginIcon from "../icons/plugin.svg"; import PluginIcon from "../icons/plugin.svg";
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
import FileUploadIcon from "../icons/file-upload.svg"; import FileUploadIcon from "../icons/file-upload.svg";
import { import {
@ -68,6 +69,7 @@ import {
isVisionModel, isVisionModel,
isDalle3, isDalle3,
showPlugins, showPlugins,
safeLocalStorage,
} from "../utils"; } from "../utils";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@ -110,6 +112,8 @@ import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { MultimodalContent } from "../client/api"; import { MultimodalContent } from "../client/api";
const localStorage = safeLocalStorage();
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => <LoadingIcon />, loading: () => <LoadingIcon />,
}); });
@ -189,7 +193,7 @@ function PromptToast(props: {
return ( return (
<div className={styles["prompt-toast"]} key="prompt-toast"> <div className={styles["prompt-toast"]} key="prompt-toast">
{props.showToast && ( {props.showToast && context.length > 0 && (
<div <div
className={styles["prompt-toast-inner"] + " clickable"} className={styles["prompt-toast-inner"] + " clickable"}
role="button" role="button"
@ -438,6 +442,7 @@ export function ChatActions(props: {
showPromptHints: () => void; showPromptHints: () => void;
hitBottom: boolean; hitBottom: boolean;
uploading: boolean; uploading: boolean;
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
}) { }) {
const config = useAppConfig(); const config = useAppConfig();
const navigate = useNavigate(); const navigate = useNavigate();
@ -503,6 +508,8 @@ export function ChatActions(props: {
const currentStyle = const currentStyle =
chatStore.currentSession().mask.modelConfig?.style ?? "vivid"; chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
const isMobileScreen = useMobileScreen();
useEffect(() => { useEffect(() => {
const show = isVisionModel(currentModel); const show = isVisionModel(currentModel);
setShowUploadImage(show); setShowUploadImage(show);
@ -618,7 +625,7 @@ export function ChatActions(props: {
items={models.map((m) => ({ items={models.map((m) => ({
title: `${m.displayName}${ title: `${m.displayName}${
m?.provider?.providerName m?.provider?.providerName
? "(" + m?.provider?.providerName + ")" ? " (" + m?.provider?.providerName + ")"
: "" : ""
}`, }`,
value: `${m.name}@${m?.provider?.providerName}`, value: `${m.name}@${m?.provider?.providerName}`,
@ -756,6 +763,14 @@ export function ChatActions(props: {
}} }}
/> />
)} )}
{!isMobileScreen && (
<ChatAction
onClick={() => props.setShowShortcutKeyModal(true)}
text={Locale.Chat.ShortcutKey.Title}
icon={<ShortcutkeyIcon />}
/>
)}
</div> </div>
); );
} }
@ -830,6 +845,67 @@ export function DeleteImageButton(props: { deleteImage: () => void }) {
); );
} }
export function ShortcutKeyModal(props: { onClose: () => void }) {
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const shortcuts = [
{
title: Locale.Chat.ShortcutKey.newChat,
keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
},
{ title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
{
title: Locale.Chat.ShortcutKey.copyLastCode,
keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
},
{
title: Locale.Chat.ShortcutKey.copyLastMessage,
keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
},
{
title: Locale.Chat.ShortcutKey.showShortcutKey,
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
},
];
return (
<div className="modal-mask">
<Modal
title={Locale.Chat.ShortcutKey.Title}
onClose={props.onClose}
actions={[
<IconButton
type="primary"
text={Locale.UI.Confirm}
icon={<ConfirmIcon />}
key="ok"
onClick={() => {
props.onClose();
}}
/>,
]}
>
<div className={styles["shortcut-key-container"]}>
<div className={styles["shortcut-key-grid"]}>
{shortcuts.map((shortcut, index) => (
<div key={index} className={styles["shortcut-key-item"]}>
<div className={styles["shortcut-key-title"]}>
{shortcut.title}
</div>
<div className={styles["shortcut-key-keys"]}>
{shortcut.keys.map((key, i) => (
<div key={i} className={styles["shortcut-key"]}>
<span>{key}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
</Modal>
</div>
);
}
function _Chat() { function _Chat() {
type RenderMessage = ChatMessage & { preview?: boolean }; type RenderMessage = ChatMessage & { preview?: boolean };
@ -942,7 +1018,7 @@ function _Chat() {
.onUserInput(userInput, attachImages) .onUserInput(userInput, attachImages)
.then(() => setIsLoading(false)); .then(() => setIsLoading(false));
setAttachImages([]); setAttachImages([]);
localStorage.setItem(LAST_INPUT_KEY, userInput); chatStore.setLastInput(userInput);
setUserInput(""); setUserInput("");
setPromptHints([]); setPromptHints([]);
if (!isMobileScreen) inputRef.current?.focus(); if (!isMobileScreen) inputRef.current?.focus();
@ -1008,7 +1084,7 @@ function _Chat() {
userInput.length <= 0 && userInput.length <= 0 &&
!(e.metaKey || e.altKey || e.ctrlKey) !(e.metaKey || e.altKey || e.ctrlKey)
) { ) {
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? ""); setUserInput(chatStore.lastInput ?? "");
e.preventDefault(); e.preventDefault();
return; return;
} }
@ -1374,6 +1450,70 @@ function _Chat() {
setAttachImages(images); setAttachImages(images);
} }
// 快捷键 shortcut keys
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
useEffect(() => {
const handleKeyDown = (event: any) => {
// 打开新聊天 command + shift + o
if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.key.toLowerCase() === "o"
) {
event.preventDefault();
setTimeout(() => {
chatStore.newSession();
navigate(Path.Chat);
}, 10);
}
// 聚焦聊天输入 shift + esc
else if (event.shiftKey && event.key.toLowerCase() === "escape") {
event.preventDefault();
inputRef.current?.focus();
}
// 复制最后一个代码块 command + shift + ;
else if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.code === "Semicolon"
) {
event.preventDefault();
const copyCodeButton =
document.querySelectorAll<HTMLElement>(".copy-code-button");
if (copyCodeButton.length > 0) {
copyCodeButton[copyCodeButton.length - 1].click();
}
}
// 复制最后一个回复 command + shift + c
else if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.key.toLowerCase() === "c"
) {
event.preventDefault();
const lastNonUserMessage = messages
.filter((message) => message.role !== "user")
.pop();
if (lastNonUserMessage) {
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
copyToClipboard(lastMessageContent);
}
}
// 展示快捷键 command + /
else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
event.preventDefault();
setShowShortcutKeyModal(true);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [messages, chatStore, navigate]);
const [showDragOverlay, setShowDragOverlay] = useState(false); const [showDragOverlay, setShowDragOverlay] = useState(false);
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
@ -1749,6 +1889,7 @@ function _Chat() {
setUserInput("/"); setUserInput("/");
onSearch(""); onSearch("");
}} }}
setShowShortcutKeyModal={setShowShortcutKeyModal}
/> />
<label <label
className={`${styles["chat-input-panel-inner"]} ${ className={`${styles["chat-input-panel-inner"]} ${
@ -1820,6 +1961,10 @@ function _Chat() {
}} }}
/> />
)} )}
{showShortcutKeyModal && (
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
)}
</div> </div>
); );
} }

View File

@ -36,7 +36,8 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
if (props.model) { if (props.model) {
return ( return (
<div className="no-dark"> <div className="no-dark">
{props.model?.startsWith("gpt-4") ? ( {props.model?.startsWith("gpt-4") ||
props.model?.startsWith("chatgpt-4o") ? (
<BlackBotIcon className="user-avatar" /> <BlackBotIcon className="user-avatar" />
) : ( ) : (
<BotIcon className="user-avatar" /> <BotIcon className="user-avatar" />

View File

@ -8,6 +8,7 @@ import { ISSUE_URL } from "../constant";
import Locale from "../locales"; import Locale from "../locales";
import { showConfirm } from "./ui-lib"; import { showConfirm } from "./ui-lib";
import { useSyncStore } from "../store/sync"; import { useSyncStore } from "../store/sync";
import { useChatStore } from "../store/chat";
interface IErrorBoundaryState { interface IErrorBoundaryState {
hasError: boolean; hasError: boolean;
@ -30,8 +31,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
try { try {
useSyncStore.getState().export(); useSyncStore.getState().export();
} finally { } finally {
localStorage.clear(); useChatStore.getState().clearAllData();
location.reload();
} }
} }

View File

@ -237,9 +237,26 @@ function escapeBrackets(text: string) {
); );
} }
function tryWrapHtmlCode(text: string) {
// try add wrap html code (fixed: html codeblock include 2 newline)
return text
.replace(
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
(match, quoteStart, lang, newLine, doctype) => {
return !quoteStart ? "\n```html\n" + doctype : match;
},
)
.replace(
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
},
);
}
function _MarkDownContent(props: { content: string }) { function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => { const escapedContent = useMemo(() => {
return escapeBrackets(escapeDollarNumber(props.content)); return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
}, [props.content]); }, [props.content]);
return ( return (

View File

@ -426,16 +426,7 @@ export function MaskPage() {
const maskStore = useMaskStore(); const maskStore = useMaskStore();
const chatStore = useChatStore(); const chatStore = useChatStore();
const [filterLang, setFilterLang] = useState<Lang | undefined>( const filterLang = maskStore.language;
() => localStorage.getItem("Mask-language") as Lang | undefined,
);
useEffect(() => {
if (filterLang) {
localStorage.setItem("Mask-language", filterLang);
} else {
localStorage.removeItem("Mask-language");
}
}, [filterLang]);
const allMasks = maskStore const allMasks = maskStore
.getAll() .getAll()
@ -542,9 +533,9 @@ export function MaskPage() {
onChange={(e) => { onChange={(e) => {
const value = e.currentTarget.value; const value = e.currentTarget.value;
if (value === Locale.Settings.Lang.All) { if (value === Locale.Settings.Lang.All) {
setFilterLang(undefined); maskStore.setLanguage(undefined);
} else { } else {
setFilterLang(value as Lang); maskStore.setLanguage(value as Lang);
} }
}} }}
> >

View File

@ -5,13 +5,19 @@ import Locale from "../locales";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib"; import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { groupBy } from "lodash-es";
export function ModelConfigList(props: { export function ModelConfigList(props: {
modelConfig: ModelConfig; modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void; updateConfig: (updater: (config: ModelConfig) => void) => void;
}) { }) {
const allModels = useAllModels(); const allModels = useAllModels();
const groupModels = groupBy(
allModels.filter((v) => v.available),
"provider.providerName",
);
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
return ( return (
<> <>
@ -19,6 +25,7 @@ export function ModelConfigList(props: {
<Select <Select
aria-label={Locale.Settings.Model} aria-label={Locale.Settings.Model}
value={value} value={value}
align="left"
onChange={(e) => { onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@"); const [model, providerName] = e.currentTarget.value.split("@");
props.updateConfig((config) => { props.updateConfig((config) => {
@ -27,13 +34,15 @@ export function ModelConfigList(props: {
}); });
}} }}
> >
{allModels {Object.keys(groupModels).map((providerName, index) => (
.filter((v) => v.available) <optgroup label={providerName} key={index}>
.map((v, i) => ( {groupModels[providerName].map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}> <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName}) {v.displayName}
</option> </option>
))} ))}
</optgroup>
))}
</Select> </Select>
</ListItem> </ListItem>
<ListItem <ListItem
@ -228,6 +237,30 @@ export function ModelConfigList(props: {
} }
></input> ></input>
</ListItem> </ListItem>
<ListItem
title={Locale.Settings.CompressModel.Title}
subTitle={Locale.Settings.CompressModel.SubTitle}
>
<Select
aria-label={Locale.Settings.CompressModel.Title}
value={compressModelValue}
onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@");
props.updateConfig((config) => {
config.compressModel = ModalConfigValidator.model(model);
config.compressProviderName = providerName as ServiceProvider;
});
}}
>
{allModels
.filter((v) => v.available)
.map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName})
</option>
))}
</Select>
</ListItem>
</> </>
); );
} }

View File

@ -252,6 +252,12 @@
position: relative; position: relative;
max-width: fit-content; max-width: fit-content;
&.left-align-option {
option {
text-align: left;
}
}
.select-with-icon-select { .select-with-icon-select {
height: 100%; height: 100%;
border: var(--border-in-light); border: var(--border-in-light);

View File

@ -292,13 +292,19 @@ export function PasswordInput(
export function Select( export function Select(
props: React.DetailedHTMLProps< props: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>, React.SelectHTMLAttributes<HTMLSelectElement> & {
align?: "left" | "center";
},
HTMLSelectElement HTMLSelectElement
>, >,
) { ) {
const { className, children, ...otherProps } = props; const { className, children, align, ...otherProps } = props;
return ( return (
<div className={`${styles["select-with-icon"]} ${className}`}> <div
className={`${styles["select-with-icon"]} ${
align === "left" ? styles["left-align-option"] : ""
} ${className}`}
>
<select className={styles["select-with-icon-select"]} {...otherProps}> <select className={styles["select-with-icon-select"]} {...otherProps}>
{children} {children}
</select> </select>

View File

@ -120,12 +120,15 @@ export const getServerSideConfig = () => {
if (disableGPT4) { if (disableGPT4) {
if (customModels) customModels += ","; if (customModels) customModels += ",";
customModels += DEFAULT_MODELS.filter( customModels += DEFAULT_MODELS.filter(
(m) => m.name.startsWith("gpt-4") && !m.name.startsWith("gpt-4o-mini"), (m) =>
(m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o")) &&
!m.name.startsWith("gpt-4o-mini"),
) )
.map((m) => "-" + m.name) .map((m) => "-" + m.name)
.join(","); .join(",");
if ( if (
defaultModel.startsWith("gpt-4") && (defaultModel.startsWith("gpt-4") ||
defaultModel.startsWith("chatgpt-4o")) &&
!defaultModel.startsWith("gpt-4o-mini") !defaultModel.startsWith("gpt-4o-mini")
) )
defaultModel = ""; defaultModel = "";

View File

@ -246,9 +246,12 @@ export const KnowledgeCutOffDate: Record<string, string> = {
"gpt-4o": "2023-10", "gpt-4o": "2023-10",
"gpt-4o-2024-05-13": "2023-10", "gpt-4o-2024-05-13": "2023-10",
"gpt-4o-2024-08-06": "2023-10", "gpt-4o-2024-08-06": "2023-10",
"chatgpt-4o-latest": "2023-10",
"gpt-4o-mini": "2023-10", "gpt-4o-mini": "2023-10",
"gpt-4o-mini-2024-07-18": "2023-10", "gpt-4o-mini-2024-07-18": "2023-10",
"gpt-4-vision-preview": "2023-04", "gpt-4-vision-preview": "2023-04",
"o1-mini": "2023-10",
"o1-preview": "2023-10",
// After improvements, // After improvements,
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously. // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
"gemini-pro": "2023-12", "gemini-pro": "2023-12",
@ -268,12 +271,15 @@ const openaiModels = [
"gpt-4o", "gpt-4o",
"gpt-4o-2024-05-13", "gpt-4o-2024-05-13",
"gpt-4o-2024-08-06", "gpt-4o-2024-08-06",
"chatgpt-4o-latest",
"gpt-4o-mini", "gpt-4o-mini",
"gpt-4o-mini-2024-07-18", "gpt-4o-mini-2024-07-18",
"gpt-4-vision-preview", "gpt-4-vision-preview",
"gpt-4-turbo-2024-04-09", "gpt-4-turbo-2024-04-09",
"gpt-4-1106-preview", "gpt-4-1106-preview",
"dall-e-3", "dall-e-3",
"o1-mini",
"o1-preview",
]; ];
const googleModels = [ const googleModels = [

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V37C4 38.1046 4.89543 39 6 39H42C43.1046 39 44 38.1046 44 37V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"/><path d="M12 19H14" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 19H23" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M29 19H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 28H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 734 B

View File

@ -41,7 +41,11 @@ export default function RootLayout({
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<link rel="manifest" href="/site.webmanifest"></link> <link
rel="manifest"
href="/site.webmanifest"
crossOrigin="use-credentials"
></link>
<script src="/serviceWorkerRegister.js" defer></script> <script src="/serviceWorkerRegister.js" defer></script>
</head> </head>
<body> <body>

View File

@ -404,6 +404,10 @@ const ar: PartialLocaleType = {
}, },
Model: "النموذج", Model: "النموذج",
CompressModel: {
Title: "نموذج الضغط",
SubTitle: "النموذج المستخدم لضغط السجل التاريخي",
},
Temperature: { Temperature: {
Title: "العشوائية (temperature)", Title: "العشوائية (temperature)",
SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود", SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود",

View File

@ -411,6 +411,10 @@ const bn: PartialLocaleType = {
}, },
Model: "মডেল (model)", Model: "মডেল (model)",
CompressModel: {
Title: "সংকোচন মডেল",
SubTitle: "ইতিহাস সংকুচিত করার জন্য ব্যবহৃত মডেল",
},
Temperature: { Temperature: {
Title: "যাদুকরিতা (temperature)", Title: "যাদুকরিতা (temperature)",
SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে", SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে",

View File

@ -1,3 +1,4 @@
import { ShortcutKeyModal } from "../components/chat";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
@ -83,6 +84,14 @@ const cn = {
SaveAs: "存为面具", SaveAs: "存为面具",
}, },
IsContext: "预设提示词", IsContext: "预设提示词",
ShortcutKey: {
Title: "键盘快捷方式",
newChat: "打开新聊天",
focusInput: "聚焦输入框",
copyLastMessage: "复制最后一个回复",
copyLastCode: "复制最后一个代码块",
showShortcutKey: "显示快捷方式",
},
}, },
Export: { Export: {
Title: "分享聊天记录", Title: "分享聊天记录",
@ -463,6 +472,10 @@ const cn = {
}, },
Model: "模型 (model)", Model: "模型 (model)",
CompressModel: {
Title: "压缩模型",
SubTitle: "用于压缩历史记录的模型",
},
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机", SubTitle: "值越大,回复越随机",
@ -497,8 +510,8 @@ const cn = {
}, },
}, },
Copy: { Copy: {
Success: "已写入剪板", Success: "已写入剪板",
Failed: "复制失败,请赋予剪板权限", Failed: "复制失败,请赋予剪板权限",
}, },
Download: { Download: {
Success: "内容已下载到您的目录。", Success: "内容已下载到您的目录。",

View File

@ -410,6 +410,10 @@ const cs: PartialLocaleType = {
}, },
Model: "Model (model)", Model: "Model (model)",
CompressModel: {
Title: "Kompresní model",
SubTitle: "Model používaný pro kompresi historie",
},
Temperature: { Temperature: {
Title: "Náhodnost (temperature)", Title: "Náhodnost (temperature)",
SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi", SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi",

View File

@ -421,6 +421,10 @@ const de: PartialLocaleType = {
}, },
Model: "Modell", Model: "Modell",
CompressModel: {
Title: "Kompressionsmodell",
SubTitle: "Modell zur Komprimierung des Verlaufs",
},
Temperature: { Temperature: {
Title: "Zufälligkeit (temperature)", Title: "Zufälligkeit (temperature)",
SubTitle: "Je höher der Wert, desto zufälliger die Antwort", SubTitle: "Je höher der Wert, desto zufälliger die Antwort",

View File

@ -85,6 +85,14 @@ const en: LocaleType = {
SaveAs: "Save as Mask", SaveAs: "Save as Mask",
}, },
IsContext: "Contextual Prompt", IsContext: "Contextual Prompt",
ShortcutKey: {
Title: "Keyboard Shortcuts",
newChat: "Open New Chat",
focusInput: "Focus Input Field",
copyLastMessage: "Copy Last Reply",
copyLastCode: "Copy Last Code Block",
showShortcutKey: "Show Shortcuts",
},
}, },
Export: { Export: {
Title: "Export Messages", Title: "Export Messages",
@ -468,6 +476,10 @@ const en: LocaleType = {
}, },
Model: "Model", Model: "Model",
CompressModel: {
Title: "Compression Model",
SubTitle: "Model used to compress history",
},
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",
SubTitle: "A larger value makes the more random output", SubTitle: "A larger value makes the more random output",

View File

@ -423,6 +423,10 @@ const es: PartialLocaleType = {
}, },
Model: "Modelo (model)", Model: "Modelo (model)",
CompressModel: {
Title: "Modelo de compresión",
SubTitle: "Modelo utilizado para comprimir el historial",
},
Temperature: { Temperature: {
Title: "Aleatoriedad (temperature)", Title: "Aleatoriedad (temperature)",
SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado", SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado",

View File

@ -422,6 +422,10 @@ const fr: PartialLocaleType = {
}, },
Model: "Modèle", Model: "Modèle",
CompressModel: {
Title: "Modèle de compression",
SubTitle: "Modèle utilisé pour compresser l'historique",
},
Temperature: { Temperature: {
Title: "Aléatoire (temperature)", Title: "Aléatoire (temperature)",
SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires", SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires",

View File

@ -411,6 +411,10 @@ const id: PartialLocaleType = {
}, },
Model: "Model", Model: "Model",
CompressModel: {
Title: "Model Kompresi",
SubTitle: "Model yang digunakan untuk mengompres riwayat",
},
Temperature: { Temperature: {
Title: "Randomness (temperature)", Title: "Randomness (temperature)",
SubTitle: "Semakin tinggi nilainya, semakin acak responsnya", SubTitle: "Semakin tinggi nilainya, semakin acak responsnya",

View File

@ -18,10 +18,13 @@ import ar from "./ar";
import bn from "./bn"; import bn from "./bn";
import sk from "./sk"; import sk from "./sk";
import { merge } from "../utils/merge"; import { merge } from "../utils/merge";
import { safeLocalStorage } from "@/app/utils";
import type { LocaleType } from "./cn"; import type { LocaleType } from "./cn";
export type { LocaleType, PartialLocaleType } from "./cn"; export type { LocaleType, PartialLocaleType } from "./cn";
const localStorage = safeLocalStorage();
const ALL_LANGS = { const ALL_LANGS = {
cn, cn,
en, en,
@ -82,17 +85,11 @@ merge(fallbackLang, targetLang);
export default fallbackLang as LocaleType; export default fallbackLang as LocaleType;
function getItem(key: string) { function getItem(key: string) {
try {
return localStorage.getItem(key); return localStorage.getItem(key);
} catch {
return null;
}
} }
function setItem(key: string, value: string) { function setItem(key: string, value: string) {
try {
localStorage.setItem(key, value); localStorage.setItem(key, value);
} catch {}
} }
function getLanguage() { function getLanguage() {

View File

@ -423,6 +423,10 @@ const it: PartialLocaleType = {
}, },
Model: "Modello (model)", Model: "Modello (model)",
CompressModel: {
Title: "Modello di compressione",
SubTitle: "Modello utilizzato per comprimere la cronologia",
},
Temperature: { Temperature: {
Title: "Casualità (temperature)", Title: "Casualità (temperature)",
SubTitle: "Valore più alto, risposte più casuali", SubTitle: "Valore più alto, risposte più casuali",

View File

@ -407,6 +407,10 @@ const jp: PartialLocaleType = {
}, },
Model: "モデル (model)", Model: "モデル (model)",
CompressModel: {
Title: "圧縮モデル",
SubTitle: "履歴を圧縮するために使用されるモデル",
},
Temperature: { Temperature: {
Title: "ランダム性 (temperature)", Title: "ランダム性 (temperature)",
SubTitle: "値が大きいほど応答がランダムになります", SubTitle: "値が大きいほど応答がランダムになります",

View File

@ -404,6 +404,10 @@ const ko: PartialLocaleType = {
}, },
Model: "모델 (model)", Model: "모델 (model)",
CompressModel: {
Title: "압축 모델",
SubTitle: "기록을 압축하는 데 사용되는 모델",
},
Temperature: { Temperature: {
Title: "무작위성 (temperature)", Title: "무작위성 (temperature)",
SubTitle: "값이 클수록 응답이 더 무작위적", SubTitle: "값이 클수록 응답이 더 무작위적",

View File

@ -415,6 +415,10 @@ const no: PartialLocaleType = {
}, },
Model: "Modell", Model: "Modell",
CompressModel: {
Title: "Komprimeringsmodell",
SubTitle: "Modell brukt for å komprimere historikken",
},
Temperature: { Temperature: {
Title: "Tilfeldighet (temperature)", Title: "Tilfeldighet (temperature)",
SubTitle: "Høyere verdi gir mer tilfeldige svar", SubTitle: "Høyere verdi gir mer tilfeldige svar",

View File

@ -346,6 +346,10 @@ const pt: PartialLocaleType = {
}, },
Model: "Modelo", Model: "Modelo",
CompressModel: {
Title: "Modelo de Compressão",
SubTitle: "Modelo usado para comprimir o histórico",
},
Temperature: { Temperature: {
Title: "Temperatura", Title: "Temperatura",
SubTitle: "Um valor maior torna a saída mais aleatória", SubTitle: "Um valor maior torna a saída mais aleatória",

View File

@ -414,6 +414,10 @@ const ru: PartialLocaleType = {
}, },
Model: "Модель", Model: "Модель",
CompressModel: {
Title: "Модель сжатия",
SubTitle: "Модель, используемая для сжатия истории",
},
Temperature: { Temperature: {
Title: "Случайность (temperature)", Title: "Случайность (temperature)",
SubTitle: "Чем больше значение, тем более случайные ответы", SubTitle: "Чем больше значение, тем более случайные ответы",

View File

@ -365,6 +365,10 @@ const sk: PartialLocaleType = {
}, },
Model: "Model", Model: "Model",
CompressModel: {
Title: "Kompresný model",
SubTitle: "Model používaný na kompresiu histórie",
},
Temperature: { Temperature: {
Title: "Teplota", Title: "Teplota",
SubTitle: "Vyššia hodnota robí výstup náhodnejším", SubTitle: "Vyššia hodnota robí výstup náhodnejším",

View File

@ -414,6 +414,10 @@ const tr: PartialLocaleType = {
}, },
Model: "Model (model)", Model: "Model (model)",
CompressModel: {
Title: "Sıkıştırma Modeli",
SubTitle: "Geçmişi sıkıştırmak için kullanılan model",
},
Temperature: { Temperature: {
Title: "Rastgelelik (temperature)", Title: "Rastgelelik (temperature)",
SubTitle: "Değer arttıkça yanıt daha rastgele olur", SubTitle: "Değer arttıkça yanıt daha rastgele olur",

View File

@ -83,6 +83,14 @@ const tw = {
SaveAs: "另存新檔", SaveAs: "另存新檔",
}, },
IsContext: "預設提示詞", IsContext: "預設提示詞",
ShortcutKey: {
Title: "鍵盤快捷方式",
newChat: "打開新聊天",
focusInput: "聚焦輸入框",
copyLastMessage: "複製最後一個回覆",
copyLastCode: "複製最後一個代碼塊",
showShortcutKey: "顯示快捷方式",
},
}, },
Export: { Export: {
Title: "將聊天記錄匯出為 Markdown", Title: "將聊天記錄匯出為 Markdown",
@ -362,6 +370,10 @@ const tw = {
}, },
Model: "模型 (model)", Model: "模型 (model)",
CompressModel: {
Title: "壓縮模型",
SubTitle: "用於壓縮歷史記錄的模型",
},
Temperature: { Temperature: {
Title: "隨機性 (temperature)", Title: "隨機性 (temperature)",
SubTitle: "值越大,回應越隨機", SubTitle: "值越大,回應越隨機",

View File

@ -410,6 +410,10 @@ const vi: PartialLocaleType = {
}, },
Model: "Mô hình (model)", Model: "Mô hình (model)",
CompressModel: {
Title: "Mô hình nén",
SubTitle: "Mô hình được sử dụng để nén lịch sử",
},
Temperature: { Temperature: {
Title: "Độ ngẫu nhiên (temperature)", Title: "Độ ngẫu nhiên (temperature)",
SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên", SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên",

View File

@ -1,33 +1,31 @@
import { trimTopic, getMessageTextContent } from "../utils"; import { getMessageTextContent, trimTopic } from "../utils";
import Locale, { getLang } from "../locales"; import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
import { nanoid } from "nanoid";
import type {
ClientApi,
MultimodalContent,
RequestMessage,
} from "../client/api";
import { getClientApi } from "../client/api";
import { ChatControllerPool } from "../client/controller";
import { showToast } from "../components/ui-lib"; import { showToast } from "../components/ui-lib";
import { ModelConfig, ModelType, useAppConfig } from "./config";
import { createEmptyMask, Mask } from "./mask";
import { import {
DEFAULT_INPUT_TEMPLATE, DEFAULT_INPUT_TEMPLATE,
DEFAULT_MODELS, DEFAULT_MODELS,
DEFAULT_SYSTEM_TEMPLATE, DEFAULT_SYSTEM_TEMPLATE,
KnowledgeCutOffDate, KnowledgeCutOffDate,
StoreKey, StoreKey,
SUMMARIZE_MODEL,
GEMINI_SUMMARIZE_MODEL,
} from "../constant"; } from "../constant";
import { getClientApi } from "../client/api"; import Locale, { getLang } from "../locales";
import type { import { isDalle3, safeLocalStorage } from "../utils";
ClientApi,
RequestMessage,
MultimodalContent,
} from "../client/api";
import { ChatControllerPool } from "../client/controller";
import { prettyObject } from "../utils/format"; import { prettyObject } from "../utils/format";
import { estimateTokenLength } from "../utils/token";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
import { collectModelsWithDefaultModel } from "../utils/model"; import { estimateTokenLength } from "../utils/token";
import { useAccessStore } from "./access"; import { ModelConfig, ModelType, useAppConfig } from "./config";
import { isDalle3 } from "../utils"; import { createEmptyMask, Mask } from "./mask";
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
const localStorage = safeLocalStorage();
export type ChatMessageTool = { export type ChatMessageTool = {
id: string; id: string;
@ -104,27 +102,6 @@ function createEmptySession(): ChatSession {
}; };
} }
function getSummarizeModel(currentModel: string) {
// if it is using gpt-* models, force to use 4o-mini to summarize
if (currentModel.startsWith("gpt")) {
const configStore = useAppConfig.getState();
const accessStore = useAccessStore.getState();
const allModel = collectModelsWithDefaultModel(
configStore.models,
[configStore.customModels, accessStore.customModels].join(","),
accessStore.defaultModel,
);
const summarizeModel = allModel.find(
(m) => m.name === SUMMARIZE_MODEL && m.available,
);
return summarizeModel?.name ?? currentModel;
}
if (currentModel.startsWith("gemini")) {
return GEMINI_SUMMARIZE_MODEL;
}
return currentModel;
}
function countMessages(msgs: ChatMessage[]) { function countMessages(msgs: ChatMessage[]) {
return msgs.reduce( return msgs.reduce(
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)), (pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
@ -179,6 +156,7 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
const DEFAULT_CHAT_STATE = { const DEFAULT_CHAT_STATE = {
sessions: [createEmptySession()], sessions: [createEmptySession()],
currentSessionIndex: 0, currentSessionIndex: 0,
lastInput: "",
}; };
export const useChatStore = createPersistStore( export const useChatStore = createPersistStore(
@ -476,7 +454,8 @@ export const useChatStore = createPersistStore(
// system prompts, to get close to OpenAI Web ChatGPT // system prompts, to get close to OpenAI Web ChatGPT
const shouldInjectSystemPrompts = const shouldInjectSystemPrompts =
modelConfig.enableInjectSystemPrompts && modelConfig.enableInjectSystemPrompts &&
session.mask.modelConfig.model.startsWith("gpt-"); (session.mask.modelConfig.model.startsWith("gpt-") ||
session.mask.modelConfig.model.startsWith("chatgpt-"));
var systemPrompts: ChatMessage[] = []; var systemPrompts: ChatMessage[] = [];
systemPrompts = shouldInjectSystemPrompts systemPrompts = shouldInjectSystemPrompts
@ -577,7 +556,7 @@ export const useChatStore = createPersistStore(
return; return;
} }
const providerName = modelConfig.providerName; const providerName = modelConfig.compressProviderName;
const api: ClientApi = getClientApi(providerName); const api: ClientApi = getClientApi(providerName);
// remove error messages if any // remove error messages if any
@ -599,7 +578,7 @@ export const useChatStore = createPersistStore(
api.llm.chat({ api.llm.chat({
messages: topicMessages, messages: topicMessages,
config: { config: {
model: getSummarizeModel(session.mask.modelConfig.model), model: modelConfig.compressModel,
stream: false, stream: false,
providerName, providerName,
}, },
@ -662,7 +641,7 @@ export const useChatStore = createPersistStore(
config: { config: {
...modelcfg, ...modelcfg,
stream: true, stream: true,
model: getSummarizeModel(session.mask.modelConfig.model), model: modelConfig.compressModel,
}, },
onUpdate(message) { onUpdate(message) {
session.memoryPrompt = message; session.memoryPrompt = message;
@ -700,13 +679,18 @@ export const useChatStore = createPersistStore(
localStorage.clear(); localStorage.clear();
location.reload(); location.reload();
}, },
setLastInput(lastInput: string) {
set({
lastInput,
});
},
}; };
return methods; return methods;
}, },
{ {
name: StoreKey.Chat, name: StoreKey.Chat,
version: 3.1, version: 3.2,
migrate(persistedState, version) { migrate(persistedState, version) {
const state = persistedState as any; const state = persistedState as any;
const newState = JSON.parse( const newState = JSON.parse(
@ -753,6 +737,16 @@ export const useChatStore = createPersistStore(
}); });
} }
// add default summarize model for every session
if (version < 3.2) {
newState.sessions.forEach((s) => {
const config = useAppConfig.getState();
s.mask.modelConfig.compressModel = config.modelConfig.compressModel;
s.mask.modelConfig.compressProviderName =
config.modelConfig.compressProviderName;
});
}
return newState as any; return newState as any;
}, },
}, },

View File

@ -50,7 +50,7 @@ export const DEFAULT_CONFIG = {
models: DEFAULT_MODELS as any as LLMModel[], models: DEFAULT_MODELS as any as LLMModel[],
modelConfig: { modelConfig: {
model: "gpt-3.5-turbo" as ModelType, model: "gpt-4o-mini" as ModelType,
providerName: "OpenAI" as ServiceProvider, providerName: "OpenAI" as ServiceProvider,
temperature: 0.5, temperature: 0.5,
top_p: 1, top_p: 1,
@ -60,6 +60,8 @@ export const DEFAULT_CONFIG = {
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
compressModel: "gpt-4o-mini" as ModelType,
compressProviderName: "OpenAI" as ServiceProvider,
enableInjectSystemPrompts: true, enableInjectSystemPrompts: true,
template: config?.template ?? DEFAULT_INPUT_TEMPLATE, template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
size: "1024x1024" as DalleSize, size: "1024x1024" as DalleSize,
@ -140,7 +142,7 @@ export const useAppConfig = createPersistStore(
}), }),
{ {
name: StoreKey.Config, name: StoreKey.Config,
version: 3.9, version: 4,
migrate(persistedState, version) { migrate(persistedState, version) {
const state = persistedState as ChatConfig; const state = persistedState as ChatConfig;
@ -178,6 +180,13 @@ export const useAppConfig = createPersistStore(
: config?.template ?? DEFAULT_INPUT_TEMPLATE; : config?.template ?? DEFAULT_INPUT_TEMPLATE;
} }
if (version < 4) {
state.modelConfig.compressModel =
DEFAULT_CONFIG.modelConfig.compressModel;
state.modelConfig.compressProviderName =
DEFAULT_CONFIG.modelConfig.compressProviderName;
}
return state as any; return state as any;
}, },
}, },

View File

@ -23,9 +23,12 @@ export type Mask = {
export const DEFAULT_MASK_STATE = { export const DEFAULT_MASK_STATE = {
masks: {} as Record<string, Mask>, masks: {} as Record<string, Mask>,
language: undefined as Lang | undefined,
}; };
export type MaskState = typeof DEFAULT_MASK_STATE; export type MaskState = typeof DEFAULT_MASK_STATE & {
language?: Lang | undefined;
};
export const DEFAULT_MASK_AVATAR = "gpt-bot"; export const DEFAULT_MASK_AVATAR = "gpt-bot";
export const createEmptyMask = () => export const createEmptyMask = () =>
@ -102,6 +105,11 @@ export const useMaskStore = createPersistStore(
search(text: string) { search(text: string) {
return Object.values(get().masks); return Object.values(get().masks);
}, },
setLanguage(language: Lang | undefined) {
set({
language,
});
},
}), }),
{ {
name: StoreKey.Mask, name: StoreKey.Mask,

View File

@ -199,7 +199,7 @@ export const usePluginStore = createPersistStore(
getAsTools(ids: string[]) { getAsTools(ids: string[]) {
const plugins = get().plugins; const plugins = get().plugins;
const selected = ids const selected = (ids || [])
.map((id) => plugins[id]) .map((id) => plugins[id])
.filter((i) => i) .filter((i) => i)
.map((p) => FunctionToolService.add(p)); .map((p) => FunctionToolService.add(p));

View File

@ -318,3 +318,63 @@ export function adapter(config: Record<string, unknown>) {
: path; : path;
return fetch(fetchUrl as string, { ...rest, responseType: "text" }); return fetch(fetchUrl as string, { ...rest, responseType: "text" });
} }
export function safeLocalStorage(): {
getItem: (key: string) => string | null;
setItem: (key: string, value: string) => void;
removeItem: (key: string) => void;
clear: () => void;
} {
let storage: Storage | null;
try {
if (typeof window !== "undefined" && window.localStorage) {
storage = window.localStorage;
} else {
storage = null;
}
} catch (e) {
console.error("localStorage is not available:", e);
storage = null;
}
return {
getItem(key: string): string | null {
if (storage) {
return storage.getItem(key);
} else {
console.warn(
`Attempted to get item "${key}" from localStorage, but localStorage is not available.`,
);
return null;
}
},
setItem(key: string, value: string): void {
if (storage) {
storage.setItem(key, value);
} else {
console.warn(
`Attempted to set item "${key}" in localStorage, but localStorage is not available.`,
);
}
},
removeItem(key: string): void {
if (storage) {
storage.removeItem(key);
} else {
console.warn(
`Attempted to remove item "${key}" from localStorage, but localStorage is not available.`,
);
}
},
clear(): void {
if (storage) {
storage.clear();
} else {
console.warn(
"Attempted to clear localStorage, but localStorage is not available.",
);
}
},
};
}

View File

@ -1,5 +1,8 @@
import { StateStorage } from "zustand/middleware"; import { StateStorage } from "zustand/middleware";
import { get, set, del, clear } from "idb-keyval"; import { get, set, del, clear } from "idb-keyval";
import { safeLocalStorage } from "@/app/utils";
const localStorage = safeLocalStorage();
class IndexedDBStorage implements StateStorage { class IndexedDBStorage implements StateStorage {
public async getItem(name: string): Promise<string | null> { public async getItem(name: string): Promise<string | null> {

View File

@ -9,7 +9,7 @@
}, },
"package": { "package": {
"productName": "NextChat", "productName": "NextChat",
"version": "2.15.0" "version": "2.15.2"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {