mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-01 23:56:39 +08:00
Merge remote-tracking branch 'upstream/main' into dev
# Conflicts: # app/components/chat.tsx
This commit is contained in:
commit
9142cd3303
@ -719,3 +719,51 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
color: #7d7d7d !important;
|
color: #7d7d7d !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
@ -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 UploadIcon from "../icons/upload.svg";
|
// import UploadIcon from "../icons/upload.svg";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -70,6 +71,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";
|
||||||
@ -121,6 +123,8 @@ import { white } from "kleur/colors";
|
|||||||
// const VoiceInput = dynamic(
|
// const VoiceInput = dynamic(
|
||||||
// () => import('@/app/components/voice-input'), { ssr: false });
|
// () => import('@/app/components/voice-input'), { ssr: false });
|
||||||
|
|
||||||
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||||
loading: () => <LoadingIcon />,
|
loading: () => <LoadingIcon />,
|
||||||
});
|
});
|
||||||
@ -460,6 +464,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();
|
||||||
@ -782,6 +787,12 @@ export function ChatActions(props: {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ChatAction
|
||||||
|
onClick={() => props.setShowShortcutKeyModal(true)}
|
||||||
|
text={Locale.Chat.ShortcutKey.Title}
|
||||||
|
icon={<ShortcutkeyIcon />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -856,6 +867,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 };
|
||||||
|
|
||||||
@ -972,7 +1044,7 @@ function _Chat() {
|
|||||||
})
|
})
|
||||||
.then(() => setIsLoading(false));
|
.then(() => setIsLoading(false));
|
||||||
setAttachImages([]);
|
setAttachImages([]);
|
||||||
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
chatStore.setLastInput(userInput);
|
||||||
setUserInput("");
|
setUserInput("");
|
||||||
setMjImageMode("IMAGINE");
|
setMjImageMode("IMAGINE");
|
||||||
setPromptHints([]);
|
setPromptHints([]);
|
||||||
@ -1039,7 +1111,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;
|
||||||
}
|
}
|
||||||
@ -1410,7 +1482,6 @@ function _Chat() {
|
|||||||
}
|
}
|
||||||
setAttachImages(images);
|
setAttachImages(images);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载状态结束,获取token
|
// 加载状态结束,获取token
|
||||||
const [loadingChange, setLoadingChange] = useState(false);
|
const [loadingChange, setLoadingChange] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -1439,20 +1510,69 @@ function _Chat() {
|
|||||||
};
|
};
|
||||||
}, [isLoading, loadingChange]);
|
}, [isLoading, loadingChange]);
|
||||||
|
|
||||||
// const [ voiceInputText, setVoiceInputText ] = useState("");
|
// 快捷键 shortcut keys
|
||||||
// const [ voiceInputLoading, setVoiceInputLoading ] = useState(false);
|
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
|
||||||
|
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// if (voiceInputLoading) {
|
const handleKeyDown = (event: any) => {
|
||||||
// // 正在进行语音输入,输入框应该显示原有文本加上语音输入的。
|
// 打开新聊天 command + shift + o
|
||||||
// setUserInput(userInput + voiceInputText);
|
if (
|
||||||
// } else {
|
(event.metaKey || event.ctrlKey) &&
|
||||||
// // 但是语音输入结束,应该清理多余字符。
|
event.shiftKey &&
|
||||||
// console.log('end', userInput, voiceInputText)
|
event.key.toLowerCase() === "o"
|
||||||
// }
|
) {
|
||||||
//
|
event.preventDefault();
|
||||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
setTimeout(() => {
|
||||||
// }, [voiceInputLoading, voiceInputText]);
|
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]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chat} key={session.id}>
|
<div className={styles.chat} key={session.id}>
|
||||||
@ -1902,6 +2022,7 @@ function _Chat() {
|
|||||||
setUserInput("/");
|
setUserInput("/");
|
||||||
onSearch("");
|
onSearch("");
|
||||||
}}
|
}}
|
||||||
|
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className={`${styles["chat-input-panel-inner"]} ${
|
className={`${styles["chat-input-panel-inner"]} ${
|
||||||
@ -1986,6 +2107,10 @@ function _Chat() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showShortcutKeyModal && (
|
||||||
|
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
1
app/icons/shortcutkey.svg
Normal file
1
app/icons/shortcutkey.svg
Normal 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 |
@ -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";
|
||||||
|
|
||||||
@ -126,6 +127,14 @@ const cn = {
|
|||||||
SaveAs: "存为面具",
|
SaveAs: "存为面具",
|
||||||
},
|
},
|
||||||
IsContext: "预设提示词",
|
IsContext: "预设提示词",
|
||||||
|
ShortcutKey: {
|
||||||
|
Title: "键盘快捷方式",
|
||||||
|
newChat: "打开新聊天",
|
||||||
|
focusInput: "聚焦输入框",
|
||||||
|
copyLastMessage: "复制最后一个回复",
|
||||||
|
copyLastCode: "复制最后一个代码块",
|
||||||
|
showShortcutKey: "显示快捷方式",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Export: {
|
Export: {
|
||||||
Title: "分享聊天记录",
|
Title: "分享聊天记录",
|
||||||
|
@ -128,6 +128,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",
|
||||||
|
@ -18,10 +18,13 @@ import en from "./en";
|
|||||||
// 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() {
|
||||||
|
@ -81,6 +81,14 @@ const tw = {
|
|||||||
SaveAs: "另存新檔",
|
SaveAs: "另存新檔",
|
||||||
},
|
},
|
||||||
IsContext: "預設提示詞",
|
IsContext: "預設提示詞",
|
||||||
|
ShortcutKey: {
|
||||||
|
Title: "鍵盤快捷方式",
|
||||||
|
newChat: "打開新聊天",
|
||||||
|
focusInput: "聚焦輸入框",
|
||||||
|
copyLastMessage: "複製最後一個回覆",
|
||||||
|
copyLastCode: "複製最後一個代碼塊",
|
||||||
|
showShortcutKey: "顯示快捷方式",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Export: {
|
Export: {
|
||||||
Title: "將聊天記錄匯出為 Markdown",
|
Title: "將聊天記錄匯出為 Markdown",
|
||||||
|
@ -31,9 +31,11 @@ import { nanoid } from "nanoid";
|
|||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { useAccessStore } from "./access";
|
import { useAccessStore } from "./access";
|
||||||
import { isDalle3 } from "../utils";
|
import { isDalle3, safeLocalStorage } from "../utils";
|
||||||
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||||
|
|
||||||
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
export type ChatMessageTool = {
|
export type ChatMessageTool = {
|
||||||
id: string;
|
id: string;
|
||||||
index?: number;
|
index?: number;
|
||||||
@ -199,6 +201,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(
|
||||||
@ -1058,6 +1061,11 @@ export const useChatStore = createPersistStore(
|
|||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
location.reload();
|
location.reload();
|
||||||
},
|
},
|
||||||
|
setLastInput(lastInput: string) {
|
||||||
|
set({
|
||||||
|
lastInput,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return methods;
|
return methods;
|
||||||
|
@ -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,
|
||||||
|
60
app/utils.ts
60
app/utils.ts
@ -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.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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> {
|
||||||
|
Loading…
Reference in New Issue
Block a user