Merge branch 'Yidadaa:main' into main
This commit is contained in:
commit
89a9a79af4
|
@ -114,7 +114,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||||
OPENAI_API_KEY=<your api key here>
|
OPENAI_API_KEY=<your api key here>
|
||||||
|
|
||||||
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
||||||
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
|
BASE_URL=https://chatgpt2.nextweb.fun/api/proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
### 本地开发
|
### 本地开发
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [protocol, ...subpath] = params.path;
|
||||||
|
const targetUrl = `${protocol}://${subpath.join("/")}`;
|
||||||
|
|
||||||
|
const method = req.headers.get("method") ?? undefined;
|
||||||
|
const shouldNotHaveBody = ["get", "head"].includes(
|
||||||
|
method?.toLowerCase() ?? "",
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
authorization: req.headers.get("authorization") ?? "",
|
||||||
|
},
|
||||||
|
body: shouldNotHaveBody ? null : req.body,
|
||||||
|
method,
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[Any Proxy]", targetUrl);
|
||||||
|
|
||||||
|
const fetchResult = fetch(targetUrl, fetchOptions);
|
||||||
|
|
||||||
|
return fetchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
|
@ -15,6 +15,7 @@ export function AuthPage() {
|
||||||
const access = useAccessStore();
|
const access = useAccessStore();
|
||||||
|
|
||||||
const goHome = () => navigate(Path.Home);
|
const goHome = () => navigate(Path.Home);
|
||||||
|
const resetAccessCode = () => access.updateCode(""); // Reset access code to empty string
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (getClientConfig()?.isApp) {
|
if (getClientConfig()?.isApp) {
|
||||||
|
@ -48,7 +49,10 @@ export function AuthPage() {
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={goHome}
|
onClick={goHome}
|
||||||
/>
|
/>
|
||||||
<IconButton text={Locale.Auth.Later} onClick={goHome} />
|
<IconButton text={Locale.Auth.Later} onClick={() => {
|
||||||
|
resetAccessCode();
|
||||||
|
goHome();
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -80,6 +80,7 @@ import {
|
||||||
MAX_RENDER_MSG_COUNT,
|
MAX_RENDER_MSG_COUNT,
|
||||||
Path,
|
Path,
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
|
UNFINISHED_INPUT,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { Avatar } from "./emoji";
|
import { Avatar } from "./emoji";
|
||||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||||
|
@ -935,7 +936,8 @@ function _Chat() {
|
||||||
|
|
||||||
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
|
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
|
||||||
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
|
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
|
||||||
const isHitBottom = bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10);
|
const isHitBottom =
|
||||||
|
bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10);
|
||||||
|
|
||||||
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
|
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
|
||||||
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
|
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
|
||||||
|
@ -1013,6 +1015,23 @@ function _Chat() {
|
||||||
// edit / insert message modal
|
// edit / insert message modal
|
||||||
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
||||||
|
|
||||||
|
// remember unfinished input
|
||||||
|
useEffect(() => {
|
||||||
|
// try to load from local storage
|
||||||
|
const key = UNFINISHED_INPUT(session.id);
|
||||||
|
const mayBeUnfinishedInput = localStorage.getItem(key);
|
||||||
|
if (mayBeUnfinishedInput && userInput.length === 0) {
|
||||||
|
setUserInput(mayBeUnfinishedInput);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dom = inputRef.current;
|
||||||
|
return () => {
|
||||||
|
localStorage.setItem(key, dom?.value ?? "");
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.chat} key={session.id}>
|
<div className={styles.chat} key={session.id}>
|
||||||
<div className="window-header" data-tauri-drag-region>
|
<div className="window-header" data-tauri-drag-region>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import GithubIcon from "../icons/github.svg";
|
||||||
import ResetIcon from "../icons/reload.svg";
|
import ResetIcon from "../icons/reload.svg";
|
||||||
import { ISSUE_URL } from "../constant";
|
import { ISSUE_URL } from "../constant";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { downloadAs } from "../utils";
|
|
||||||
import { showConfirm } from "./ui-lib";
|
import { showConfirm } from "./ui-lib";
|
||||||
|
import { useSyncStore } from "../store/sync";
|
||||||
|
|
||||||
interface IErrorBoundaryState {
|
interface IErrorBoundaryState {
|
||||||
hasError: boolean;
|
hasError: boolean;
|
||||||
|
@ -26,10 +26,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||||
|
|
||||||
clearAndSaveData() {
|
clearAndSaveData() {
|
||||||
try {
|
try {
|
||||||
downloadAs(
|
useSyncStore.getState().export();
|
||||||
JSON.stringify(localStorage),
|
|
||||||
"chatgpt-next-web-snapshot.json",
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|
|
@ -410,7 +410,7 @@ export function MaskPage() {
|
||||||
const closeMaskModal = () => setEditingMaskId(undefined);
|
const closeMaskModal = () => setEditingMaskId(undefined);
|
||||||
|
|
||||||
const downloadAll = () => {
|
const downloadAll = () => {
|
||||||
downloadAs(JSON.stringify(masks), FileName.Masks);
|
downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks);
|
||||||
};
|
};
|
||||||
|
|
||||||
const importFromFile = () => {
|
const importFromFile = () => {
|
||||||
|
@ -452,11 +452,13 @@ export function MaskPage() {
|
||||||
icon={<DownloadIcon />}
|
icon={<DownloadIcon />}
|
||||||
bordered
|
bordered
|
||||||
onClick={downloadAll}
|
onClick={downloadAll}
|
||||||
|
text={Locale.UI.Export}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="window-action-button">
|
<div className="window-action-button">
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<UploadIcon />}
|
icon={<UploadIcon />}
|
||||||
|
text={Locale.UI.Import}
|
||||||
bordered
|
bordered
|
||||||
onClick={() => importFromFile()}
|
onClick={() => importFromFile()}
|
||||||
/>
|
/>
|
||||||
|
@ -604,7 +606,7 @@ export function MaskPage() {
|
||||||
<MaskConfig
|
<MaskConfig
|
||||||
mask={editingMask}
|
mask={editingMask}
|
||||||
updateMask={(updater) =>
|
updateMask={(updater) =>
|
||||||
maskStore.update(editingMaskId!, updater)
|
maskStore.updateMask(editingMaskId!, updater)
|
||||||
}
|
}
|
||||||
readonly={editingMask.builtin}
|
readonly={editingMask.builtin}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,6 +10,15 @@ import ClearIcon from "../icons/clear.svg";
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
import EditIcon from "../icons/edit.svg";
|
import EditIcon from "../icons/edit.svg";
|
||||||
import EyeIcon from "../icons/eye.svg";
|
import EyeIcon from "../icons/eye.svg";
|
||||||
|
import DownloadIcon from "../icons/download.svg";
|
||||||
|
import UploadIcon from "../icons/upload.svg";
|
||||||
|
import ConfigIcon from "../icons/config.svg";
|
||||||
|
import ConfirmIcon from "../icons/confirm.svg";
|
||||||
|
|
||||||
|
import ConnectionIcon from "../icons/connection.svg";
|
||||||
|
import CloudSuccessIcon from "../icons/cloud-success.svg";
|
||||||
|
import CloudFailIcon from "../icons/cloud-fail.svg";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
List,
|
List,
|
||||||
|
@ -19,6 +28,7 @@ import {
|
||||||
Popover,
|
Popover,
|
||||||
Select,
|
Select,
|
||||||
showConfirm,
|
showConfirm,
|
||||||
|
showToast,
|
||||||
} from "./ui-lib";
|
} from "./ui-lib";
|
||||||
import { ModelConfigList } from "./model-config";
|
import { ModelConfigList } from "./model-config";
|
||||||
|
|
||||||
|
@ -49,6 +59,8 @@ import { Avatar, AvatarPicker } from "./emoji";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { useSyncStore } from "../store/sync";
|
import { useSyncStore } from "../store/sync";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import { useMaskStore } from "../store/mask";
|
||||||
|
import { ProviderType } from "../utils/cloud";
|
||||||
|
|
||||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
|
@ -75,7 +87,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||||
readOnly={!prompt.isUser}
|
readOnly={!prompt.isUser}
|
||||||
className={styles["edit-prompt-title"]}
|
className={styles["edit-prompt-title"]}
|
||||||
onInput={(e) =>
|
onInput={(e) =>
|
||||||
promptStore.update(
|
promptStore.updatePrompt(
|
||||||
props.id,
|
props.id,
|
||||||
(prompt) => (prompt.title = e.currentTarget.value),
|
(prompt) => (prompt.title = e.currentTarget.value),
|
||||||
)
|
)
|
||||||
|
@ -87,7 +99,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||||
className={styles["edit-prompt-content"]}
|
className={styles["edit-prompt-content"]}
|
||||||
rows={10}
|
rows={10}
|
||||||
onInput={(e) =>
|
onInput={(e) =>
|
||||||
promptStore.update(
|
promptStore.updatePrompt(
|
||||||
props.id,
|
props.id,
|
||||||
(prompt) => (prompt.content = e.currentTarget.value),
|
(prompt) => (prompt.content = e.currentTarget.value),
|
||||||
)
|
)
|
||||||
|
@ -127,14 +139,15 @@ function UserPromptModal(props: { onClose?: () => void }) {
|
||||||
actions={[
|
actions={[
|
||||||
<IconButton
|
<IconButton
|
||||||
key="add"
|
key="add"
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
promptStore.add({
|
const promptId = promptStore.add({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
title: "Empty Prompt",
|
title: "Empty Prompt",
|
||||||
content: "Empty Prompt Content",
|
content: "Empty Prompt Content",
|
||||||
})
|
});
|
||||||
}
|
setEditingPromptId(promptId);
|
||||||
|
}}
|
||||||
icon={<AddIcon />}
|
icon={<AddIcon />}
|
||||||
bordered
|
bordered
|
||||||
text={Locale.Settings.Prompt.Modal.Add}
|
text={Locale.Settings.Prompt.Modal.Add}
|
||||||
|
@ -241,75 +254,262 @@ function DangerItems() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SyncItems() {
|
function CheckButton() {
|
||||||
const syncStore = useSyncStore();
|
const syncStore = useSyncStore();
|
||||||
const webdav = syncStore.webDavConfig;
|
|
||||||
|
|
||||||
// not ready: https://github.com/Yidadaa/ChatGPT-Next-Web/issues/920#issuecomment-1609866332
|
const couldCheck = useMemo(() => {
|
||||||
return null;
|
return syncStore.coundSync();
|
||||||
|
}, [syncStore]);
|
||||||
|
|
||||||
|
const [checkState, setCheckState] = useState<
|
||||||
|
"none" | "checking" | "success" | "failed"
|
||||||
|
>("none");
|
||||||
|
|
||||||
|
async function check() {
|
||||||
|
setCheckState("checking");
|
||||||
|
const valid = await syncStore.check();
|
||||||
|
setCheckState(valid ? "success" : "failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!couldCheck) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<IconButton
|
||||||
<ListItem
|
text="检查可用性"
|
||||||
title={"上次同步:" + new Date().toLocaleString()}
|
bordered
|
||||||
subTitle={"20 次对话,100 条消息,200 提示词,20 面具"}
|
onClick={check}
|
||||||
|
icon={
|
||||||
|
checkState === "none" ? (
|
||||||
|
<ConnectionIcon />
|
||||||
|
) : checkState === "checking" ? (
|
||||||
|
<LoadingIcon />
|
||||||
|
) : checkState === "success" ? (
|
||||||
|
<CloudSuccessIcon />
|
||||||
|
) : checkState === "failed" ? (
|
||||||
|
<CloudFailIcon />
|
||||||
|
) : (
|
||||||
|
<ConnectionIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SyncConfigModal(props: { onClose?: () => void }) {
|
||||||
|
const syncStore = useSyncStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.Settings.Sync.Config.Modal.Title}
|
||||||
|
onClose={() => props.onClose?.()}
|
||||||
|
actions={[
|
||||||
|
<CheckButton key="check" />,
|
||||||
|
<IconButton
|
||||||
|
key="confirm"
|
||||||
|
onClick={props.onClose}
|
||||||
|
icon={<ConfirmIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.UI.Confirm}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<IconButton
|
<List>
|
||||||
icon={<ResetIcon />}
|
<ListItem
|
||||||
text="同步"
|
title={Locale.Settings.Sync.Config.SyncType.Title}
|
||||||
onClick={() => {
|
subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
|
||||||
syncStore.check().then(console.log);
|
>
|
||||||
}}
|
<select
|
||||||
/>
|
value={syncStore.provider}
|
||||||
</ListItem>
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) =>
|
||||||
|
(config.provider = e.target.value as ProviderType),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(ProviderType).map(([k, v]) => (
|
||||||
|
<option value={v} key={k}>
|
||||||
|
{k}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={"本地备份"}
|
title={Locale.Settings.Sync.Config.Proxy.Title}
|
||||||
subTitle={"20 次对话,100 条消息,200 提示词,20 面具"}
|
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
||||||
></ListItem>
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={syncStore.useProxy}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) => (config.useProxy = e.currentTarget.checked),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
{syncStore.useProxy ? (
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Sync.Config.ProxyUrl.Title}
|
||||||
|
subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={syncStore.proxyUrl}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) => (config.proxyUrl = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
) : null}
|
||||||
|
</List>
|
||||||
|
|
||||||
<ListItem
|
{syncStore.provider === ProviderType.WebDAV && (
|
||||||
title={"Web Dav Server"}
|
<>
|
||||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
<List>
|
||||||
>
|
<ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
|
||||||
<input
|
<input
|
||||||
value={webdav.server}
|
type="text"
|
||||||
type="text"
|
value={syncStore.webdav.endpoint}
|
||||||
placeholder={"https://example.com"}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
syncStore.update(
|
||||||
syncStore.update(
|
(config) =>
|
||||||
(config) => (config.server = e.currentTarget.value),
|
(config.webdav.endpoint = e.currentTarget.value),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem title="Web Dav User Name" subTitle="user name here">
|
<ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
|
||||||
<input
|
<input
|
||||||
value={webdav.username}
|
type="text"
|
||||||
type="text"
|
value={syncStore.webdav.username}
|
||||||
placeholder={"username"}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
syncStore.update(
|
||||||
syncStore.update(
|
(config) =>
|
||||||
(config) => (config.username = e.currentTarget.value),
|
(config.webdav.username = e.currentTarget.value),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
></input>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
|
||||||
|
<PasswordInput
|
||||||
|
value={syncStore.webdav.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) =>
|
||||||
|
(config.webdav.password = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></PasswordInput>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<ListItem title="Web Dav Password" subTitle="password here">
|
{syncStore.provider === ProviderType.UpStash && (
|
||||||
<input
|
<List>
|
||||||
value={webdav.password}
|
<ListItem title={Locale.WIP}></ListItem>
|
||||||
type="text"
|
</List>
|
||||||
placeholder={"password"}
|
)}
|
||||||
onChange={(e) => {
|
</Modal>
|
||||||
syncStore.update(
|
</div>
|
||||||
(config) => (config.password = e.currentTarget.value),
|
);
|
||||||
);
|
}
|
||||||
}}
|
|
||||||
/>
|
function SyncItems() {
|
||||||
</ListItem>
|
const syncStore = useSyncStore();
|
||||||
</List>
|
const chatStore = useChatStore();
|
||||||
|
const promptStore = usePromptStore();
|
||||||
|
const maskStore = useMaskStore();
|
||||||
|
const couldSync = useMemo(() => {
|
||||||
|
return syncStore.coundSync();
|
||||||
|
}, [syncStore]);
|
||||||
|
|
||||||
|
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
|
||||||
|
|
||||||
|
const stateOverview = useMemo(() => {
|
||||||
|
const sessions = chatStore.sessions;
|
||||||
|
const messageCount = sessions.reduce((p, c) => p + c.messages.length, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat: sessions.length,
|
||||||
|
message: messageCount,
|
||||||
|
prompt: Object.keys(promptStore.prompts).length,
|
||||||
|
mask: Object.keys(maskStore.masks).length,
|
||||||
|
};
|
||||||
|
}, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<List>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Sync.CloudState}
|
||||||
|
subTitle={
|
||||||
|
syncStore.lastProvider
|
||||||
|
? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
|
||||||
|
syncStore.lastProvider
|
||||||
|
}]`
|
||||||
|
: Locale.Settings.Sync.NotSyncYet
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<IconButton
|
||||||
|
icon={<ConfigIcon />}
|
||||||
|
text={Locale.UI.Config}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSyncConfigModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{couldSync && (
|
||||||
|
<IconButton
|
||||||
|
icon={<ResetIcon />}
|
||||||
|
text={Locale.UI.Sync}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await syncStore.sync();
|
||||||
|
showToast(Locale.Settings.Sync.Success);
|
||||||
|
} catch (e) {
|
||||||
|
showToast(Locale.Settings.Sync.Fail);
|
||||||
|
console.error("[Sync]", e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Sync.LocalState}
|
||||||
|
subTitle={Locale.Settings.Sync.Overview(stateOverview)}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<IconButton
|
||||||
|
icon={<UploadIcon />}
|
||||||
|
text={Locale.UI.Export}
|
||||||
|
onClick={() => {
|
||||||
|
syncStore.export();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<DownloadIcon />}
|
||||||
|
text={Locale.UI.Import}
|
||||||
|
onClick={() => {
|
||||||
|
syncStore.import();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{showSyncConfigModal && (
|
||||||
|
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,6 +762,8 @@ export function Settings() {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
<SyncItems />
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Mask.Splash.Title}
|
title={Locale.Settings.Mask.Splash.Title}
|
||||||
|
@ -710,8 +912,6 @@ export function Settings() {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<SyncItems />
|
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
<ModelConfigList
|
<ModelConfigList
|
||||||
modelConfig={config.modelConfig}
|
modelConfig={config.modelConfig}
|
||||||
|
|
|
@ -7,7 +7,9 @@ export const RELEASE_URL = `${REPO_URL}/releases`;
|
||||||
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
||||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||||
export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy";
|
|
||||||
|
export const DEFAULT_CORS_HOST = "https://chatgpt2.nextweb.fun";
|
||||||
|
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
|
||||||
|
|
||||||
export enum Path {
|
export enum Path {
|
||||||
Home = "/",
|
Home = "/",
|
||||||
|
@ -18,6 +20,10 @@ export enum Path {
|
||||||
Auth = "/auth",
|
Auth = "/auth",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ApiPath {
|
||||||
|
Cors = "/api/cors",
|
||||||
|
}
|
||||||
|
|
||||||
export enum SlotID {
|
export enum SlotID {
|
||||||
AppBody = "app-body",
|
AppBody = "app-body",
|
||||||
}
|
}
|
||||||
|
@ -44,6 +50,9 @@ export const NARROW_SIDEBAR_WIDTH = 100;
|
||||||
export const ACCESS_CODE_PREFIX = "nk-";
|
export const ACCESS_CODE_PREFIX = "nk-";
|
||||||
|
|
||||||
export const LAST_INPUT_KEY = "last-input";
|
export const LAST_INPUT_KEY = "last-input";
|
||||||
|
export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
|
||||||
|
|
||||||
|
export const STORAGE_KEY = "chatgpt-next-web";
|
||||||
|
|
||||||
export const REQUEST_TIMEOUT_MS = 60000;
|
export const REQUEST_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.2 KiB |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use transform="translate(0 0) rotate(0)" xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M4.00337,11.6633M4.00337,11.6633c-0.8391,0 -1.5514,-0.20763 -2.13691,-0.6229c-0.79984,-0.56727 -1.19975,-1.41519 -1.19975,-2.54377c0,-1.2521 0.52073,-2.20348 1.56218,-2.85415c0.68561,-0.42835 1.38711,-0.64252 2.10448,-0.64252v0.66667l-0.64163,-0.18098l0.64163,0.18098l-0.64163,-0.18098c0.65543,-2.32379 2.09264,-3.48569 4.31163,-3.48569c2.30635,0 3.74023,0.99523 4.30163,2.98569l-0.6416,0.18098l0.0729,-0.66267l-0.0729,0.66267l0.0729,-0.66267c2.3958,0.26354 3.5937,1.5411 3.5937,3.83267c0,2.21778 -1.10887,3.32667 -3.3266,3.32667c-0.0438,0 -0.08717,-0.00427 -0.1301,-0.0128c-0.04293,-0.00853 -0.0846,-0.0212 -0.125,-0.038c-0.04047,-0.01673 -0.0789,-0.03727 -0.1153,-0.0616c-0.0364,-0.02427 -0.07007,-0.0519 -0.101,-0.0829c-0.031,-0.03093 -0.05863,-0.0646 -0.0829,-0.101c-0.02433,-0.0364 -0.04487,-0.07483 -0.0616,-0.1153c-0.0168,-0.0404 -0.02947,-0.08207 -0.038,-0.125c-0.00853,-0.04293 -0.0128,-0.0863 -0.0128,-0.1301c0,-0.04373 0.00427,-0.08707 0.0128,-0.13c0.00853,-0.04293 0.0212,-0.08463 0.038,-0.1251c0.01673,-0.04047 0.03727,-0.0789 0.0616,-0.1153c0.02427,-0.0364 0.0519,-0.07007 0.0829,-0.101c0.03093,-0.03093 0.0646,-0.05857 0.101,-0.0829c0.0364,-0.02433 0.07483,-0.04487 0.1153,-0.0616c0.0404,-0.01673 0.08207,-0.02937 0.125,-0.0379c0.04293,-0.00853 0.0863,-0.0128 0.1301,-0.0128c1.32887,0 1.9933,-0.66446 1.9933,-1.99337c0,-1.4951 -0.80207,-2.33088 -2.4062,-2.50733c-0.27,-0.02971 -0.495,-0.22026 0,0c-0.27,-0.02971 -0.495,-0.22026 -0.5688,-0.4817c-0.37873,-1.34287 -1.38484,-2.01431 -3.01833,-2.01431c-1.54613,0 -2.55559,0.8381 -3.02836,2.51431c-0.08103,0.28728 -0.34314,0.48569 0,0c-0.08103,0.28728 -0.34314,0.48569 -0.64164,0.48569c-0.46252,0 -0.92852,0.14666 -1.39801,0.43998c-0.62355,0.38957 -0.93532,0.96402 -0.93532,1.72336c0,0.66927 0.21258,1.15467 0.63775,1.45621c0.35449,0.25144 0.80969,0.37716 1.36558,0.37716c0.04378,0 0.08713,0.00427 0.13006,0.0128c0.04293,0.00853 0.08462,0.02117 0.12507,0.0379c0.04044,0.01673 0.07886,0.03727 0.11525,0.0616c0.0364,0.02433 0.07008,0.05197 0.10103,0.0829c0.03095,0.03093 0.05859,0.0646 0.08291,0.101c0.02432,0.0364 0.04485,0.07483 0.0616,0.1153c0.01675,0.04047 0.0294,0.08217 0.03794,0.1251c0.00854,0.04293 0.01281,0.08627 0.01281,0.13c0,0.0438 -0.00427,0.08717 -0.01281,0.1301c-0.00854,0.04293 -0.02119,0.0846 -0.03794,0.125c-0.01675,0.04047 -0.03728,0.0789 -0.0616,0.1153c-0.02432,0.0364 -0.05196,0.07007 -0.08291,0.101c-0.03095,0.031 -0.06463,0.05863 -0.10103,0.0829c-0.03639,0.02433 -0.07481,0.04487 -0.11525,0.0616c-0.04045,0.0168 -0.08214,0.02947 -0.12507,0.038c-0.04293,0.00853 -0.08628,0.0128 -0.13006,0.0128z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M6.42578,10.4903l2,1.66l-0.42578,0.513l-0.52012,-0.4171l2.67002,-3.32998c0.0274,-0.03415 0.05783,-0.06531 0.0913,-0.09346c0.03353,-0.02815 0.0695,-0.05277 0.1079,-0.07384c0.03833,-0.02107 0.07837,-0.0382 0.1201,-0.05138c0.04173,-0.01319 0.08437,-0.02217 0.1279,-0.02696c0.04353,-0.00479 0.0871,-0.00528 0.1307,-0.00149c0.0436,0.00379 0.0864,0.01181 0.1284,0.02404c0.04207,0.01223 0.0825,0.02844 0.1213,0.04863c0.03887,0.0202 0.07537,0.04399 0.1095,0.07137c0.0342,0.02738 0.06537,0.05783 0.0935,0.09135c0.02813,0.03352 0.05273,0.06946 0.0738,0.10783c0.02107,0.03837 0.0382,0.07843 0.0514,0.12017c0.0132,0.04174 0.0222,0.08437 0.027,0.12788c0.00473,0.04351 0.00523,0.08707 0.0015,0.13068c-0.0038,0.04361 -0.01183,0.08643 -0.0241,0.12846c-0.0122,0.04203 -0.0284,0.08247 -0.0486,0.1213c-0.0202,0.03884 -0.044,0.07534 -0.0714,0.10949l-2.66998,3.33001c-0.23308,0.2907 -0.65919,0.3339 -0.9459,0.0959l-2,-1.66c-0.03369,-0.02793 -0.06432,-0.0589 -0.0919,-0.0929c-0.02758,-0.034 -0.05158,-0.07037 -0.072,-0.1091c-0.02042,-0.03867 -0.03687,-0.079 -0.04934,-0.121c-0.01248,-0.04193 -0.02074,-0.0847 -0.02479,-0.1283c-0.00405,-0.0436 -0.0038,-0.08717 0.00073,-0.1307c0.00453,-0.04353 0.01326,-0.0862 0.0262,-0.128c0.01294,-0.0418 0.02983,-0.08197 0.05068,-0.1205c0.02085,-0.03847 0.04526,-0.07453 0.07321,-0.1082c0.02796,-0.03373 0.05893,-0.06437 0.09292,-0.0919c0.03399,-0.0276 0.07035,-0.0516 0.10907,-0.072c0.03872,-0.02047 0.07906,-0.03693 0.12102,-0.0494c0.04196,-0.01247 0.08473,-0.02073 0.12831,-0.0248c0.04359,-0.004 0.08715,-0.00373 0.13069,0.0008c0.04354,0.00453 0.08622,0.01327 0.12804,0.0262c0.04181,0.01293 0.08197,0.02983 0.12046,0.0507c0.03849,0.0208 0.07457,0.0452 0.10826,0.0732z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>
|
After Width: | Height: | Size: 4.7 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 15 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.1 KiB |
|
@ -178,6 +178,42 @@ const cn = {
|
||||||
Title: "自动生成标题",
|
Title: "自动生成标题",
|
||||||
SubTitle: "根据对话内容生成合适的标题",
|
SubTitle: "根据对话内容生成合适的标题",
|
||||||
},
|
},
|
||||||
|
Sync: {
|
||||||
|
CloudState: "云端数据",
|
||||||
|
NotSyncYet: "还没有进行过同步",
|
||||||
|
Success: "同步成功",
|
||||||
|
Fail: "同步失败",
|
||||||
|
|
||||||
|
Config: {
|
||||||
|
Modal: {
|
||||||
|
Title: "配置云同步",
|
||||||
|
},
|
||||||
|
SyncType: {
|
||||||
|
Title: "同步类型",
|
||||||
|
SubTitle: "选择喜爱的同步服务器",
|
||||||
|
},
|
||||||
|
Proxy: {
|
||||||
|
Title: "启用代理",
|
||||||
|
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||||
|
},
|
||||||
|
ProxyUrl: {
|
||||||
|
Title: "代理地址",
|
||||||
|
SubTitle: "仅适用于本项目自带的跨域代理",
|
||||||
|
},
|
||||||
|
|
||||||
|
WebDav: {
|
||||||
|
Endpoint: "WebDAV 地址",
|
||||||
|
UserName: "用户名",
|
||||||
|
Password: "密码",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
LocalState: "本地数据",
|
||||||
|
Overview: (overview: any) => {
|
||||||
|
return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`;
|
||||||
|
},
|
||||||
|
ImportFailed: "导入失败",
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Splash: {
|
Splash: {
|
||||||
Title: "面具启动页",
|
Title: "面具启动页",
|
||||||
|
@ -355,6 +391,10 @@ const cn = {
|
||||||
Close: "关闭",
|
Close: "关闭",
|
||||||
Create: "新建",
|
Create: "新建",
|
||||||
Edit: "编辑",
|
Edit: "编辑",
|
||||||
|
Export: "导出",
|
||||||
|
Import: "导入",
|
||||||
|
Sync: "同步",
|
||||||
|
Config: "配置",
|
||||||
},
|
},
|
||||||
Exporter: {
|
Exporter: {
|
||||||
Model: "模型",
|
Model: "模型",
|
||||||
|
|
|
@ -180,6 +180,43 @@ const en: LocaleType = {
|
||||||
Title: "Auto Generate Title",
|
Title: "Auto Generate Title",
|
||||||
SubTitle: "Generate a suitable title based on the conversation content",
|
SubTitle: "Generate a suitable title based on the conversation content",
|
||||||
},
|
},
|
||||||
|
Sync: {
|
||||||
|
CloudState: "Last Update",
|
||||||
|
NotSyncYet: "Not sync yet",
|
||||||
|
Success: "Sync Success",
|
||||||
|
Fail: "Sync Fail",
|
||||||
|
|
||||||
|
Config: {
|
||||||
|
Modal: {
|
||||||
|
Title: "Config Sync",
|
||||||
|
},
|
||||||
|
SyncType: {
|
||||||
|
Title: "Sync Type",
|
||||||
|
SubTitle: "Choose your favorite sync service",
|
||||||
|
},
|
||||||
|
Proxy: {
|
||||||
|
Title: "Enable CORS Proxy",
|
||||||
|
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
|
||||||
|
},
|
||||||
|
ProxyUrl: {
|
||||||
|
Title: "Proxy Endpoint",
|
||||||
|
SubTitle:
|
||||||
|
"Only applicable to the built-in CORS proxy for this project",
|
||||||
|
},
|
||||||
|
|
||||||
|
WebDav: {
|
||||||
|
Endpoint: "WebDAV Endpoint",
|
||||||
|
UserName: "User Name",
|
||||||
|
Password: "Password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
LocalState: "Local Data",
|
||||||
|
Overview: (overview: any) => {
|
||||||
|
return `${overview.chat} chats,${overview.message} messages,${overview.prompt} prompts,${overview.mask} masks`;
|
||||||
|
},
|
||||||
|
ImportFailed: "Failed to import from file",
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Splash: {
|
Splash: {
|
||||||
Title: "Mask Splash Screen",
|
Title: "Mask Splash Screen",
|
||||||
|
@ -355,6 +392,10 @@ const en: LocaleType = {
|
||||||
Close: "Close",
|
Close: "Close",
|
||||||
Create: "Create",
|
Create: "Create",
|
||||||
Edit: "Edit",
|
Edit: "Edit",
|
||||||
|
Export: "Export",
|
||||||
|
Import: "Import",
|
||||||
|
Sync: "Sync",
|
||||||
|
Config: "Config",
|
||||||
},
|
},
|
||||||
Exporter: {
|
Exporter: {
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
|
|
|
@ -19,7 +19,11 @@ const jp: PartialLocaleType = {
|
||||||
Copy: "コピー",
|
Copy: "コピー",
|
||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "リトライ",
|
Retry: "リトライ",
|
||||||
|
Pin: "ピン",
|
||||||
|
PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました",
|
||||||
|
PinToastAction: "表示",
|
||||||
Delete: "削除",
|
Delete: "削除",
|
||||||
|
Edit: "編集",
|
||||||
},
|
},
|
||||||
Rename: "チャットの名前を変更",
|
Rename: "チャットの名前を変更",
|
||||||
Typing: "入力中…",
|
Typing: "入力中…",
|
||||||
|
@ -33,7 +37,7 @@ const jp: PartialLocaleType = {
|
||||||
Send: "送信",
|
Send: "送信",
|
||||||
Config: {
|
Config: {
|
||||||
Reset: "リセット",
|
Reset: "リセット",
|
||||||
SaveAs: "另存为面具",
|
SaveAs: "保存",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Export: {
|
Export: {
|
||||||
|
|
|
@ -1,28 +1,7 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant";
|
import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant";
|
||||||
import { getHeaders } from "../client/api";
|
import { getHeaders } from "../client/api";
|
||||||
import { BOT_HELLO } from "./chat";
|
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
|
import { createPersistStore } from "../utils/store";
|
||||||
export interface AccessControlStore {
|
|
||||||
accessCode: string;
|
|
||||||
token: string;
|
|
||||||
|
|
||||||
needCode: boolean;
|
|
||||||
hideUserApiKey: boolean;
|
|
||||||
hideBalanceQuery: boolean;
|
|
||||||
disableGPT4: boolean;
|
|
||||||
|
|
||||||
openaiUrl: string;
|
|
||||||
|
|
||||||
updateToken: (_: string) => void;
|
|
||||||
updateCode: (_: string) => void;
|
|
||||||
updateOpenAiUrl: (_: string) => void;
|
|
||||||
enabledAccessControl: () => boolean;
|
|
||||||
isAuthorized: () => boolean;
|
|
||||||
fetch: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||||
|
|
||||||
|
@ -30,72 +9,74 @@ const DEFAULT_OPENAI_URL =
|
||||||
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
|
getClientConfig()?.buildMode === "export" ? DEFAULT_API_HOST : "/api/openai/";
|
||||||
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
|
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
|
||||||
|
|
||||||
export const useAccessStore = create<AccessControlStore>()(
|
const DEFAULT_ACCESS_STATE = {
|
||||||
persist(
|
token: "",
|
||||||
(set, get) => ({
|
accessCode: "",
|
||||||
token: "",
|
needCode: true,
|
||||||
accessCode: "",
|
hideUserApiKey: false,
|
||||||
needCode: true,
|
hideBalanceQuery: false,
|
||||||
hideUserApiKey: false,
|
disableGPT4: false,
|
||||||
hideBalanceQuery: false,
|
|
||||||
disableGPT4: false,
|
|
||||||
|
|
||||||
openaiUrl: DEFAULT_OPENAI_URL,
|
openaiUrl: DEFAULT_OPENAI_URL,
|
||||||
|
};
|
||||||
|
|
||||||
enabledAccessControl() {
|
export const useAccessStore = createPersistStore(
|
||||||
get().fetch();
|
{ ...DEFAULT_ACCESS_STATE },
|
||||||
|
|
||||||
return get().needCode;
|
(set, get) => ({
|
||||||
},
|
enabledAccessControl() {
|
||||||
updateCode(code: string) {
|
this.fetch();
|
||||||
set(() => ({ accessCode: code?.trim() }));
|
|
||||||
},
|
|
||||||
updateToken(token: string) {
|
|
||||||
set(() => ({ token: token?.trim() }));
|
|
||||||
},
|
|
||||||
updateOpenAiUrl(url: string) {
|
|
||||||
set(() => ({ openaiUrl: url?.trim() }));
|
|
||||||
},
|
|
||||||
isAuthorized() {
|
|
||||||
get().fetch();
|
|
||||||
|
|
||||||
// has token or has code or disabled access control
|
return get().needCode;
|
||||||
return (
|
|
||||||
!!get().token || !!get().accessCode || !get().enabledAccessControl()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
fetch() {
|
|
||||||
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
|
|
||||||
fetchState = 1;
|
|
||||||
fetch("/api/config", {
|
|
||||||
method: "post",
|
|
||||||
body: null,
|
|
||||||
headers: {
|
|
||||||
...getHeaders(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((res: DangerConfig) => {
|
|
||||||
console.log("[Config] got config from server", res);
|
|
||||||
set(() => ({ ...res }));
|
|
||||||
|
|
||||||
if (res.disableGPT4) {
|
|
||||||
DEFAULT_MODELS.forEach(
|
|
||||||
(m: any) => (m.available = !m.name.startsWith("gpt-4")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.error("[Config] failed to fetch config");
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
fetchState = 2;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: StoreKey.Access,
|
|
||||||
version: 1,
|
|
||||||
},
|
},
|
||||||
),
|
updateCode(code: string) {
|
||||||
|
set(() => ({ accessCode: code?.trim() }));
|
||||||
|
},
|
||||||
|
updateToken(token: string) {
|
||||||
|
set(() => ({ token: token?.trim() }));
|
||||||
|
},
|
||||||
|
updateOpenAiUrl(url: string) {
|
||||||
|
set(() => ({ openaiUrl: url?.trim() }));
|
||||||
|
},
|
||||||
|
isAuthorized() {
|
||||||
|
this.fetch();
|
||||||
|
|
||||||
|
// has token or has code or disabled access control
|
||||||
|
return (
|
||||||
|
!!get().token || !!get().accessCode || !this.enabledAccessControl()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
fetch() {
|
||||||
|
if (fetchState > 0 || getClientConfig()?.buildMode === "export") return;
|
||||||
|
fetchState = 1;
|
||||||
|
fetch("/api/config", {
|
||||||
|
method: "post",
|
||||||
|
body: null,
|
||||||
|
headers: {
|
||||||
|
...getHeaders(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res: DangerConfig) => {
|
||||||
|
console.log("[Config] got config from server", res);
|
||||||
|
set(() => ({ ...res }));
|
||||||
|
|
||||||
|
if (res.disableGPT4) {
|
||||||
|
DEFAULT_MODELS.forEach(
|
||||||
|
(m: any) => (m.available = !m.name.startsWith("gpt-4")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.error("[Config] failed to fetch config");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
fetchState = 2;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: StoreKey.Access,
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { ChatControllerPool } from "../client/controller";
|
||||||
import { prettyObject } from "../utils/format";
|
import { prettyObject } from "../utils/format";
|
||||||
import { estimateTokenLength } from "../utils/token";
|
import { estimateTokenLength } from "../utils/token";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import { createPersistStore } from "../utils/store";
|
||||||
|
|
||||||
export type ChatMessage = RequestMessage & {
|
export type ChatMessage = RequestMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
|
@ -140,12 +141,22 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useChatStore = create<ChatStore>()(
|
const DEFAULT_CHAT_STATE = {
|
||||||
persist(
|
sessions: [createEmptySession()],
|
||||||
(set, get) => ({
|
currentSessionIndex: 0,
|
||||||
sessions: [createEmptySession()],
|
};
|
||||||
currentSessionIndex: 0,
|
|
||||||
|
|
||||||
|
export const useChatStore = createPersistStore(
|
||||||
|
DEFAULT_CHAT_STATE,
|
||||||
|
(set, _get) => {
|
||||||
|
function get() {
|
||||||
|
return {
|
||||||
|
..._get(),
|
||||||
|
...methods,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const methods = {
|
||||||
clearSessions() {
|
clearSessions() {
|
||||||
set(() => ({
|
set(() => ({
|
||||||
sessions: [createEmptySession()],
|
sessions: [createEmptySession()],
|
||||||
|
@ -184,7 +195,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
newSession(mask) {
|
newSession(mask?: Mask) {
|
||||||
const session = createEmptySession();
|
const session = createEmptySession();
|
||||||
|
|
||||||
if (mask) {
|
if (mask) {
|
||||||
|
@ -207,14 +218,14 @@ export const useChatStore = create<ChatStore>()(
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
nextSession(delta) {
|
nextSession(delta: number) {
|
||||||
const n = get().sessions.length;
|
const n = get().sessions.length;
|
||||||
const limit = (x: number) => (x + n) % n;
|
const limit = (x: number) => (x + n) % n;
|
||||||
const i = get().currentSessionIndex;
|
const i = get().currentSessionIndex;
|
||||||
get().selectSession(limit(i + delta));
|
get().selectSession(limit(i + delta));
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteSession(index) {
|
deleteSession(index: number) {
|
||||||
const deletingLastSession = get().sessions.length === 1;
|
const deletingLastSession = get().sessions.length === 1;
|
||||||
const deletedSession = get().sessions.at(index);
|
const deletedSession = get().sessions.at(index);
|
||||||
|
|
||||||
|
@ -271,7 +282,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
|
||||||
onNewMessage(message) {
|
onNewMessage(message: ChatMessage) {
|
||||||
get().updateCurrentSession((session) => {
|
get().updateCurrentSession((session) => {
|
||||||
session.messages = session.messages.concat();
|
session.messages = session.messages.concat();
|
||||||
session.lastUpdate = Date.now();
|
session.lastUpdate = Date.now();
|
||||||
|
@ -280,7 +291,7 @@ export const useChatStore = create<ChatStore>()(
|
||||||
get().summarizeSession();
|
get().summarizeSession();
|
||||||
},
|
},
|
||||||
|
|
||||||
async onUserInput(content) {
|
async onUserInput(content: string) {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
const modelConfig = session.mask.modelConfig;
|
const modelConfig = session.mask.modelConfig;
|
||||||
|
|
||||||
|
@ -580,14 +591,14 @@ export const useChatStore = create<ChatStore>()(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateStat(message) {
|
updateStat(message: ChatMessage) {
|
||||||
get().updateCurrentSession((session) => {
|
get().updateCurrentSession((session) => {
|
||||||
session.stat.charCount += message.content.length;
|
session.stat.charCount += message.content.length;
|
||||||
// TODO: should update chat count and word count
|
// TODO: should update chat count and word count
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentSession(updater) {
|
updateCurrentSession(updater: (session: ChatSession) => void) {
|
||||||
const sessions = get().sessions;
|
const sessions = get().sessions;
|
||||||
const index = get().currentSessionIndex;
|
const index = get().currentSessionIndex;
|
||||||
updater(sessions[index]);
|
updater(sessions[index]);
|
||||||
|
@ -598,56 +609,60 @@ export const useChatStore = create<ChatStore>()(
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
location.reload();
|
location.reload();
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
{
|
|
||||||
name: StoreKey.Chat,
|
|
||||||
version: 3.1,
|
|
||||||
migrate(persistedState, version) {
|
|
||||||
const state = persistedState as any;
|
|
||||||
const newState = JSON.parse(JSON.stringify(state)) as ChatStore;
|
|
||||||
|
|
||||||
if (version < 2) {
|
return methods;
|
||||||
newState.sessions = [];
|
},
|
||||||
|
{
|
||||||
|
name: StoreKey.Chat,
|
||||||
|
version: 3.1,
|
||||||
|
migrate(persistedState, version) {
|
||||||
|
const state = persistedState as any;
|
||||||
|
const newState = JSON.parse(
|
||||||
|
JSON.stringify(state),
|
||||||
|
) as typeof DEFAULT_CHAT_STATE;
|
||||||
|
|
||||||
const oldSessions = state.sessions;
|
if (version < 2) {
|
||||||
for (const oldSession of oldSessions) {
|
newState.sessions = [];
|
||||||
const newSession = createEmptySession();
|
|
||||||
newSession.topic = oldSession.topic;
|
const oldSessions = state.sessions;
|
||||||
newSession.messages = [...oldSession.messages];
|
for (const oldSession of oldSessions) {
|
||||||
newSession.mask.modelConfig.sendMemory = true;
|
const newSession = createEmptySession();
|
||||||
newSession.mask.modelConfig.historyMessageCount = 4;
|
newSession.topic = oldSession.topic;
|
||||||
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
|
newSession.messages = [...oldSession.messages];
|
||||||
newState.sessions.push(newSession);
|
newSession.mask.modelConfig.sendMemory = true;
|
||||||
|
newSession.mask.modelConfig.historyMessageCount = 4;
|
||||||
|
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
|
||||||
|
newState.sessions.push(newSession);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 3) {
|
||||||
|
// migrate id to nanoid
|
||||||
|
newState.sessions.forEach((s) => {
|
||||||
|
s.id = nanoid();
|
||||||
|
s.messages.forEach((m) => (m.id = nanoid()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable `enableInjectSystemPrompts` attribute for old sessions.
|
||||||
|
// Resolve issue of old sessions not automatically enabling.
|
||||||
|
if (version < 3.1) {
|
||||||
|
newState.sessions.forEach((s) => {
|
||||||
|
if (
|
||||||
|
// Exclude those already set by user
|
||||||
|
!s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts")
|
||||||
|
) {
|
||||||
|
// Because users may have changed this configuration,
|
||||||
|
// the user's current configuration is used instead of the default
|
||||||
|
const config = useAppConfig.getState();
|
||||||
|
s.mask.modelConfig.enableInjectSystemPrompts =
|
||||||
|
config.modelConfig.enableInjectSystemPrompts;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (version < 3) {
|
return newState as any;
|
||||||
// migrate id to nanoid
|
|
||||||
newState.sessions.forEach((s) => {
|
|
||||||
s.id = nanoid();
|
|
||||||
s.messages.forEach((m) => (m.id = nanoid()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable `enableInjectSystemPrompts` attribute for old sessions.
|
|
||||||
// Resolve issue of old sessions not automatically enabling.
|
|
||||||
if (version < 3.1) {
|
|
||||||
newState.sessions.forEach((s) => {
|
|
||||||
if (
|
|
||||||
// Exclude those already set by user
|
|
||||||
!s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts")
|
|
||||||
) {
|
|
||||||
// Because users may have changed this configuration,
|
|
||||||
// the user's current configuration is used instead of the default
|
|
||||||
const config = useAppConfig.getState();
|
|
||||||
s.mask.modelConfig.enableInjectSystemPrompts =
|
|
||||||
config.modelConfig.enableInjectSystemPrompts;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return newState;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
import { LLMModel } from "../client/api";
|
import { LLMModel } from "../client/api";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant";
|
import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant";
|
||||||
|
import { createPersistStore } from "../utils/store";
|
||||||
|
|
||||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||||
|
|
||||||
|
@ -21,6 +20,8 @@ export enum Theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CONFIG = {
|
export const DEFAULT_CONFIG = {
|
||||||
|
lastUpdate: Date.now(), // timestamp, to merge state
|
||||||
|
|
||||||
submitKey: SubmitKey.CtrlEnter as SubmitKey,
|
submitKey: SubmitKey.CtrlEnter as SubmitKey,
|
||||||
avatar: "1f603",
|
avatar: "1f603",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
@ -55,13 +56,6 @@ export const DEFAULT_CONFIG = {
|
||||||
|
|
||||||
export type ChatConfig = typeof DEFAULT_CONFIG;
|
export type ChatConfig = typeof DEFAULT_CONFIG;
|
||||||
|
|
||||||
export type ChatConfigStore = ChatConfig & {
|
|
||||||
reset: () => void;
|
|
||||||
update: (updater: (config: ChatConfig) => void) => void;
|
|
||||||
mergeModels: (newModels: LLMModel[]) => void;
|
|
||||||
allModels: () => LLMModel[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ModelConfig = ChatConfig["modelConfig"];
|
export type ModelConfig = ChatConfig["modelConfig"];
|
||||||
|
|
||||||
export function limitNumber(
|
export function limitNumber(
|
||||||
|
@ -98,85 +92,80 @@ export const ModalConfigValidator = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAppConfig = create<ChatConfigStore>()(
|
export const useAppConfig = createPersistStore(
|
||||||
persist(
|
{ ...DEFAULT_CONFIG },
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
...DEFAULT_CONFIG,
|
reset() {
|
||||||
|
set(() => ({ ...DEFAULT_CONFIG }));
|
||||||
reset() {
|
|
||||||
set(() => ({ ...DEFAULT_CONFIG }));
|
|
||||||
},
|
|
||||||
|
|
||||||
update(updater) {
|
|
||||||
const config = { ...get() };
|
|
||||||
updater(config);
|
|
||||||
set(() => config);
|
|
||||||
},
|
|
||||||
|
|
||||||
mergeModels(newModels) {
|
|
||||||
if (!newModels || newModels.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldModels = get().models;
|
|
||||||
const modelMap: Record<string, LLMModel> = {};
|
|
||||||
|
|
||||||
for (const model of oldModels) {
|
|
||||||
model.available = false;
|
|
||||||
modelMap[model.name] = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const model of newModels) {
|
|
||||||
model.available = true;
|
|
||||||
modelMap[model.name] = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
models: Object.values(modelMap),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
allModels() {
|
|
||||||
const customModels = get()
|
|
||||||
.customModels.split(",")
|
|
||||||
.filter((v) => !!v && v.length > 0)
|
|
||||||
.map((m) => ({ name: m, available: true }));
|
|
||||||
|
|
||||||
const models = get().models.concat(customModels);
|
|
||||||
return models;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: StoreKey.Config,
|
|
||||||
version: 3.7,
|
|
||||||
migrate(persistedState, version) {
|
|
||||||
const state = persistedState as ChatConfig;
|
|
||||||
|
|
||||||
if (version < 3.4) {
|
|
||||||
state.modelConfig.sendMemory = true;
|
|
||||||
state.modelConfig.historyMessageCount = 4;
|
|
||||||
state.modelConfig.compressMessageLengthThreshold = 1000;
|
|
||||||
state.modelConfig.frequency_penalty = 0;
|
|
||||||
state.modelConfig.top_p = 1;
|
|
||||||
state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
|
|
||||||
state.dontShowMaskSplashScreen = false;
|
|
||||||
state.hideBuiltinMasks = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version < 3.5) {
|
|
||||||
state.customModels = "claude,claude-100k";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version < 3.6) {
|
|
||||||
state.modelConfig.enableInjectSystemPrompts = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version < 3.7) {
|
|
||||||
state.enableAutoGenerateTitle = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state as any;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
|
||||||
|
mergeModels(newModels: LLMModel[]) {
|
||||||
|
if (!newModels || newModels.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldModels = get().models;
|
||||||
|
const modelMap: Record<string, LLMModel> = {};
|
||||||
|
|
||||||
|
for (const model of oldModels) {
|
||||||
|
model.available = false;
|
||||||
|
modelMap[model.name] = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const model of newModels) {
|
||||||
|
model.available = true;
|
||||||
|
modelMap[model.name] = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
models: Object.values(modelMap),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
allModels() {
|
||||||
|
const customModels = get()
|
||||||
|
.customModels.split(",")
|
||||||
|
.filter((v) => !!v && v.length > 0)
|
||||||
|
.map((m) => ({ name: m, available: true }));
|
||||||
|
|
||||||
|
const models = get().models.concat(customModels);
|
||||||
|
return models;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: StoreKey.Config,
|
||||||
|
version: 3.8,
|
||||||
|
migrate(persistedState, version) {
|
||||||
|
const state = persistedState as ChatConfig;
|
||||||
|
|
||||||
|
if (version < 3.4) {
|
||||||
|
state.modelConfig.sendMemory = true;
|
||||||
|
state.modelConfig.historyMessageCount = 4;
|
||||||
|
state.modelConfig.compressMessageLengthThreshold = 1000;
|
||||||
|
state.modelConfig.frequency_penalty = 0;
|
||||||
|
state.modelConfig.top_p = 1;
|
||||||
|
state.modelConfig.template = DEFAULT_INPUT_TEMPLATE;
|
||||||
|
state.dontShowMaskSplashScreen = false;
|
||||||
|
state.hideBuiltinMasks = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 3.5) {
|
||||||
|
state.customModels = "claude,claude-100k";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 3.6) {
|
||||||
|
state.modelConfig.enableInjectSystemPrompts = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 3.7) {
|
||||||
|
state.enableAutoGenerateTitle = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 3.8) {
|
||||||
|
state.lastUpdate = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
return state as any;
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
import { BUILTIN_MASKS } from "../masks";
|
import { BUILTIN_MASKS } from "../masks";
|
||||||
import { getLang, Lang } from "../locales";
|
import { getLang, Lang } from "../locales";
|
||||||
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
|
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
|
||||||
import { ModelConfig, useAppConfig } from "./config";
|
import { ModelConfig, useAppConfig } from "./config";
|
||||||
import { StoreKey } from "../constant";
|
import { StoreKey } from "../constant";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import { createPersistStore } from "../utils/store";
|
||||||
|
|
||||||
export type Mask = {
|
export type Mask = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -25,14 +24,6 @@ export const DEFAULT_MASK_STATE = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MaskState = typeof DEFAULT_MASK_STATE;
|
export type MaskState = typeof DEFAULT_MASK_STATE;
|
||||||
type MaskStore = MaskState & {
|
|
||||||
create: (mask?: Partial<Mask>) => Mask;
|
|
||||||
update: (id: string, updater: (mask: Mask) => void) => void;
|
|
||||||
delete: (id: string) => void;
|
|
||||||
search: (text: string) => Mask[];
|
|
||||||
get: (id?: string) => Mask | null;
|
|
||||||
getAll: () => Mask[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_MASK_AVATAR = "gpt-bot";
|
export const DEFAULT_MASK_AVATAR = "gpt-bot";
|
||||||
export const createEmptyMask = () =>
|
export const createEmptyMask = () =>
|
||||||
|
@ -46,89 +37,90 @@ export const createEmptyMask = () =>
|
||||||
lang: getLang(),
|
lang: getLang(),
|
||||||
builtin: false,
|
builtin: false,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
} as Mask);
|
}) as Mask;
|
||||||
|
|
||||||
export const useMaskStore = create<MaskStore>()(
|
export const useMaskStore = createPersistStore(
|
||||||
persist(
|
{ ...DEFAULT_MASK_STATE },
|
||||||
(set, get) => ({
|
|
||||||
...DEFAULT_MASK_STATE,
|
|
||||||
|
|
||||||
create(mask) {
|
(set, get) => ({
|
||||||
const masks = get().masks;
|
create(mask?: Partial<Mask>) {
|
||||||
const id = nanoid();
|
const masks = get().masks;
|
||||||
masks[id] = {
|
const id = nanoid();
|
||||||
...createEmptyMask(),
|
masks[id] = {
|
||||||
...mask,
|
...createEmptyMask(),
|
||||||
id,
|
...mask,
|
||||||
builtin: false,
|
id,
|
||||||
};
|
builtin: false,
|
||||||
|
};
|
||||||
|
|
||||||
set(() => ({ masks }));
|
set(() => ({ masks }));
|
||||||
|
get().markUpdate();
|
||||||
|
|
||||||
return masks[id];
|
return masks[id];
|
||||||
},
|
|
||||||
update(id, updater) {
|
|
||||||
const masks = get().masks;
|
|
||||||
const mask = masks[id];
|
|
||||||
if (!mask) return;
|
|
||||||
const updateMask = { ...mask };
|
|
||||||
updater(updateMask);
|
|
||||||
masks[id] = updateMask;
|
|
||||||
set(() => ({ masks }));
|
|
||||||
},
|
|
||||||
delete(id) {
|
|
||||||
const masks = get().masks;
|
|
||||||
delete masks[id];
|
|
||||||
set(() => ({ masks }));
|
|
||||||
},
|
|
||||||
|
|
||||||
get(id) {
|
|
||||||
return get().masks[id ?? 1145141919810];
|
|
||||||
},
|
|
||||||
getAll() {
|
|
||||||
const userMasks = Object.values(get().masks).sort(
|
|
||||||
(a, b) => b.createdAt - a.createdAt,
|
|
||||||
);
|
|
||||||
const config = useAppConfig.getState();
|
|
||||||
if (config.hideBuiltinMasks) return userMasks;
|
|
||||||
const buildinMasks = BUILTIN_MASKS.map(
|
|
||||||
(m) =>
|
|
||||||
({
|
|
||||||
...m,
|
|
||||||
modelConfig: {
|
|
||||||
...config.modelConfig,
|
|
||||||
...m.modelConfig,
|
|
||||||
},
|
|
||||||
} as Mask),
|
|
||||||
);
|
|
||||||
return userMasks.concat(buildinMasks);
|
|
||||||
},
|
|
||||||
search(text) {
|
|
||||||
return Object.values(get().masks);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: StoreKey.Mask,
|
|
||||||
version: 3.1,
|
|
||||||
|
|
||||||
migrate(state, version) {
|
|
||||||
const newState = JSON.parse(JSON.stringify(state)) as MaskState;
|
|
||||||
|
|
||||||
// migrate mask id to nanoid
|
|
||||||
if (version < 3) {
|
|
||||||
Object.values(newState.masks).forEach((m) => (m.id = nanoid()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version < 3.1) {
|
|
||||||
const updatedMasks: Record<string, Mask> = {};
|
|
||||||
Object.values(newState.masks).forEach((m) => {
|
|
||||||
updatedMasks[m.id] = m;
|
|
||||||
});
|
|
||||||
newState.masks = updatedMasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
return newState as any;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
updateMask(id: string, updater: (mask: Mask) => void) {
|
||||||
|
const masks = get().masks;
|
||||||
|
const mask = masks[id];
|
||||||
|
if (!mask) return;
|
||||||
|
const updateMask = { ...mask };
|
||||||
|
updater(updateMask);
|
||||||
|
masks[id] = updateMask;
|
||||||
|
set(() => ({ masks }));
|
||||||
|
get().markUpdate();
|
||||||
|
},
|
||||||
|
delete(id: string) {
|
||||||
|
const masks = get().masks;
|
||||||
|
delete masks[id];
|
||||||
|
set(() => ({ masks }));
|
||||||
|
get().markUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
|
get(id?: string) {
|
||||||
|
return get().masks[id ?? 1145141919810];
|
||||||
|
},
|
||||||
|
getAll() {
|
||||||
|
const userMasks = Object.values(get().masks).sort(
|
||||||
|
(a, b) => b.createdAt - a.createdAt,
|
||||||
|
);
|
||||||
|
const config = useAppConfig.getState();
|
||||||
|
if (config.hideBuiltinMasks) return userMasks;
|
||||||
|
const buildinMasks = BUILTIN_MASKS.map(
|
||||||
|
(m) =>
|
||||||
|
({
|
||||||
|
...m,
|
||||||
|
modelConfig: {
|
||||||
|
...config.modelConfig,
|
||||||
|
...m.modelConfig,
|
||||||
|
},
|
||||||
|
}) as Mask,
|
||||||
|
);
|
||||||
|
return userMasks.concat(buildinMasks);
|
||||||
|
},
|
||||||
|
search(text: string) {
|
||||||
|
return Object.values(get().masks);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: StoreKey.Mask,
|
||||||
|
version: 3.1,
|
||||||
|
|
||||||
|
migrate(state, version) {
|
||||||
|
const newState = JSON.parse(JSON.stringify(state)) as MaskState;
|
||||||
|
|
||||||
|
// migrate mask id to nanoid
|
||||||
|
if (version < 3) {
|
||||||
|
Object.values(newState.masks).forEach((m) => (m.id = nanoid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version < 3.1) {
|
||||||
|
const updatedMasks: Record<string, Mask> = {};
|
||||||
|
Object.values(newState.masks).forEach((m) => {
|
||||||
|
updatedMasks[m.id] = m;
|
||||||
|
});
|
||||||
|
newState.masks = updatedMasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState as any;
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { getLang } from "../locales";
|
import { getLang } from "../locales";
|
||||||
import { StoreKey } from "../constant";
|
import { StoreKey } from "../constant";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
import { createPersistStore } from "../utils/store";
|
||||||
|
|
||||||
export interface Prompt {
|
export interface Prompt {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -13,19 +12,6 @@ export interface Prompt {
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PromptStore {
|
|
||||||
counter: number;
|
|
||||||
prompts: Record<string, Prompt>;
|
|
||||||
|
|
||||||
add: (prompt: Prompt) => string;
|
|
||||||
get: (id: string) => Prompt | undefined;
|
|
||||||
remove: (id: string) => void;
|
|
||||||
search: (text: string) => Prompt[];
|
|
||||||
update: (id: string, updater: (prompt: Prompt) => void) => void;
|
|
||||||
|
|
||||||
getUserPrompts: () => Prompt[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchService = {
|
export const SearchService = {
|
||||||
ready: false,
|
ready: false,
|
||||||
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
|
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
|
||||||
|
@ -62,130 +48,136 @@ export const SearchService = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePromptStore = create<PromptStore>()(
|
export const usePromptStore = createPersistStore(
|
||||||
persist(
|
{
|
||||||
(set, get) => ({
|
counter: 0,
|
||||||
counter: 0,
|
prompts: {} as Record<string, Prompt>,
|
||||||
latestId: 0,
|
},
|
||||||
prompts: {},
|
|
||||||
|
|
||||||
add(prompt) {
|
(set, get) => ({
|
||||||
const prompts = get().prompts;
|
add(prompt: Prompt) {
|
||||||
prompt.id = nanoid();
|
const prompts = get().prompts;
|
||||||
prompt.isUser = true;
|
prompt.id = nanoid();
|
||||||
prompt.createdAt = Date.now();
|
prompt.isUser = true;
|
||||||
prompts[prompt.id] = prompt;
|
prompt.createdAt = Date.now();
|
||||||
|
prompts[prompt.id] = prompt;
|
||||||
|
|
||||||
set(() => ({
|
set(() => ({
|
||||||
latestId: prompt.id!,
|
prompts: prompts,
|
||||||
prompts: prompts,
|
}));
|
||||||
}));
|
|
||||||
|
|
||||||
return prompt.id!;
|
return prompt.id!;
|
||||||
},
|
|
||||||
|
|
||||||
get(id) {
|
|
||||||
const targetPrompt = get().prompts[id];
|
|
||||||
|
|
||||||
if (!targetPrompt) {
|
|
||||||
return SearchService.builtinPrompts.find((v) => v.id === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetPrompt;
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(id) {
|
|
||||||
const prompts = get().prompts;
|
|
||||||
delete prompts[id];
|
|
||||||
SearchService.remove(id);
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
prompts,
|
|
||||||
counter: get().counter + 1,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
getUserPrompts() {
|
|
||||||
const userPrompts = Object.values(get().prompts ?? {});
|
|
||||||
userPrompts.sort((a, b) =>
|
|
||||||
b.id && a.id ? b.createdAt - a.createdAt : 0,
|
|
||||||
);
|
|
||||||
return userPrompts;
|
|
||||||
},
|
|
||||||
|
|
||||||
update(id, updater) {
|
|
||||||
const prompt = get().prompts[id] ?? {
|
|
||||||
title: "",
|
|
||||||
content: "",
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
|
|
||||||
SearchService.remove(id);
|
|
||||||
updater(prompt);
|
|
||||||
const prompts = get().prompts;
|
|
||||||
prompts[id] = prompt;
|
|
||||||
set(() => ({ prompts }));
|
|
||||||
SearchService.add(prompt);
|
|
||||||
},
|
|
||||||
|
|
||||||
search(text) {
|
|
||||||
if (text.length === 0) {
|
|
||||||
// return all rompts
|
|
||||||
return get().getUserPrompts().concat(SearchService.builtinPrompts);
|
|
||||||
}
|
|
||||||
return SearchService.search(text) as Prompt[];
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: StoreKey.Prompt,
|
|
||||||
version: 3,
|
|
||||||
|
|
||||||
migrate(state, version) {
|
|
||||||
const newState = JSON.parse(JSON.stringify(state)) as PromptStore;
|
|
||||||
|
|
||||||
if (version < 3) {
|
|
||||||
Object.values(newState.prompts).forEach((p) => (p.id = nanoid()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return newState;
|
|
||||||
},
|
|
||||||
|
|
||||||
onRehydrateStorage(state) {
|
|
||||||
const PROMPT_URL = "./prompts.json";
|
|
||||||
|
|
||||||
type PromptList = Array<[string, string]>;
|
|
||||||
|
|
||||||
fetch(PROMPT_URL)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((res) => {
|
|
||||||
let fetchPrompts = [res.en, res.cn];
|
|
||||||
if (getLang() === "cn") {
|
|
||||||
fetchPrompts = fetchPrompts.reverse();
|
|
||||||
}
|
|
||||||
const builtinPrompts = fetchPrompts.map(
|
|
||||||
(promptList: PromptList) => {
|
|
||||||
return promptList.map(
|
|
||||||
([title, content]) =>
|
|
||||||
({
|
|
||||||
id: nanoid(),
|
|
||||||
title,
|
|
||||||
content,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
} as Prompt),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const userPrompts =
|
|
||||||
usePromptStore.getState().getUserPrompts() ?? [];
|
|
||||||
|
|
||||||
const allPromptsForSearch = builtinPrompts
|
|
||||||
.reduce((pre, cur) => pre.concat(cur), [])
|
|
||||||
.filter((v) => !!v.title && !!v.content);
|
|
||||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
|
||||||
SearchService.init(allPromptsForSearch, userPrompts);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
),
|
|
||||||
|
get(id: string) {
|
||||||
|
const targetPrompt = get().prompts[id];
|
||||||
|
|
||||||
|
if (!targetPrompt) {
|
||||||
|
return SearchService.builtinPrompts.find((v) => v.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetPrompt;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(id: string) {
|
||||||
|
const prompts = get().prompts;
|
||||||
|
delete prompts[id];
|
||||||
|
|
||||||
|
Object.entries(prompts).some(([key, prompt]) => {
|
||||||
|
if (prompt.id === id) {
|
||||||
|
delete prompts[key];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
SearchService.remove(id);
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
prompts,
|
||||||
|
counter: get().counter + 1,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
getUserPrompts() {
|
||||||
|
const userPrompts = Object.values(get().prompts ?? {});
|
||||||
|
userPrompts.sort((a, b) =>
|
||||||
|
b.id && a.id ? b.createdAt - a.createdAt : 0,
|
||||||
|
);
|
||||||
|
return userPrompts;
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePrompt(id: string, updater: (prompt: Prompt) => void) {
|
||||||
|
const prompt = get().prompts[id] ?? {
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchService.remove(id);
|
||||||
|
updater(prompt);
|
||||||
|
const prompts = get().prompts;
|
||||||
|
prompts[id] = prompt;
|
||||||
|
set(() => ({ prompts }));
|
||||||
|
SearchService.add(prompt);
|
||||||
|
},
|
||||||
|
|
||||||
|
search(text: string) {
|
||||||
|
if (text.length === 0) {
|
||||||
|
// return all rompts
|
||||||
|
return this.getUserPrompts().concat(SearchService.builtinPrompts);
|
||||||
|
}
|
||||||
|
return SearchService.search(text) as Prompt[];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: StoreKey.Prompt,
|
||||||
|
version: 3,
|
||||||
|
|
||||||
|
migrate(state, version) {
|
||||||
|
const newState = JSON.parse(JSON.stringify(state)) as {
|
||||||
|
prompts: Record<string, Prompt>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (version < 3) {
|
||||||
|
Object.values(newState.prompts).forEach((p) => (p.id = nanoid()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState as any;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRehydrateStorage(state) {
|
||||||
|
const PROMPT_URL = "./prompts.json";
|
||||||
|
|
||||||
|
type PromptList = Array<[string, string]>;
|
||||||
|
|
||||||
|
fetch(PROMPT_URL)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
let fetchPrompts = [res.en, res.cn];
|
||||||
|
if (getLang() === "cn") {
|
||||||
|
fetchPrompts = fetchPrompts.reverse();
|
||||||
|
}
|
||||||
|
const builtinPrompts = fetchPrompts.map((promptList: PromptList) => {
|
||||||
|
return promptList.map(
|
||||||
|
([title, content]) =>
|
||||||
|
({
|
||||||
|
id: nanoid(),
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
}) as Prompt,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const userPrompts = usePromptStore.getState().getUserPrompts() ?? [];
|
||||||
|
|
||||||
|
const allPromptsForSearch = builtinPrompts
|
||||||
|
.reduce((pre, cur) => pre.concat(cur), [])
|
||||||
|
.filter((v) => !!v.title && !!v.content);
|
||||||
|
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||||
|
SearchService.init(allPromptsForSearch, userPrompts);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
import { Updater } from "../typing";
|
import { Updater } from "../typing";
|
||||||
import { create } from "zustand";
|
import { ApiPath, StoreKey } from "../constant";
|
||||||
import { persist } from "zustand/middleware";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { StoreKey } from "../constant";
|
import {
|
||||||
|
AppState,
|
||||||
|
getLocalAppState,
|
||||||
|
GetStoreState,
|
||||||
|
mergeAppState,
|
||||||
|
setLocalAppState,
|
||||||
|
} from "../utils/sync";
|
||||||
|
import { downloadAs, readFromFile } from "../utils";
|
||||||
|
import { showToast } from "../components/ui-lib";
|
||||||
|
import Locale from "../locales";
|
||||||
|
import { createSyncClient, ProviderType } from "../utils/cloud";
|
||||||
|
import { corsPath } from "../utils/cors";
|
||||||
|
|
||||||
export interface WebDavConfig {
|
export interface WebDavConfig {
|
||||||
server: string;
|
server: string;
|
||||||
|
@ -9,79 +20,94 @@ export interface WebDavConfig {
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SyncStore {
|
export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||||
webDavConfig: WebDavConfig;
|
|
||||||
lastSyncTime: number;
|
|
||||||
|
|
||||||
update: Updater<WebDavConfig>;
|
export const useSyncStore = createPersistStore(
|
||||||
check: () => Promise<boolean>;
|
{
|
||||||
|
provider: ProviderType.WebDAV,
|
||||||
|
useProxy: true,
|
||||||
|
proxyUrl: corsPath(ApiPath.Cors),
|
||||||
|
|
||||||
path: (path: string) => string;
|
webdav: {
|
||||||
headers: () => { Authorization: string };
|
endpoint: "",
|
||||||
}
|
username: "",
|
||||||
|
password: "",
|
||||||
const FILE = {
|
|
||||||
root: "/chatgpt-next-web/",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSyncStore = create<SyncStore>()(
|
|
||||||
persist(
|
|
||||||
(set, get) => ({
|
|
||||||
webDavConfig: {
|
|
||||||
server: "",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
lastSyncTime: 0,
|
|
||||||
|
|
||||||
update(updater) {
|
|
||||||
const config = { ...get().webDavConfig };
|
|
||||||
updater(config);
|
|
||||||
set({ webDavConfig: config });
|
|
||||||
},
|
|
||||||
|
|
||||||
async check() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(this.path(""), {
|
|
||||||
method: "PROFIND",
|
|
||||||
headers: this.headers(),
|
|
||||||
});
|
|
||||||
console.log(res);
|
|
||||||
return res.status === 207;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[Sync] ", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
path(path: string) {
|
|
||||||
let url = get().webDavConfig.server;
|
|
||||||
|
|
||||||
if (!url.endsWith("/")) {
|
|
||||||
url += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (path.startsWith("/")) {
|
|
||||||
path = path.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return url + path;
|
|
||||||
},
|
|
||||||
|
|
||||||
headers() {
|
|
||||||
const auth = btoa(
|
|
||||||
[get().webDavConfig.username, get().webDavConfig.password].join(":"),
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
Authorization: `Basic ${auth}`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: StoreKey.Sync,
|
|
||||||
version: 1,
|
|
||||||
},
|
},
|
||||||
),
|
|
||||||
|
upstash: {
|
||||||
|
endpoint: "",
|
||||||
|
username: "",
|
||||||
|
apiKey: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
lastSyncTime: 0,
|
||||||
|
lastProvider: "",
|
||||||
|
},
|
||||||
|
(set, get) => ({
|
||||||
|
coundSync() {
|
||||||
|
const config = get()[get().provider];
|
||||||
|
return Object.values(config).every((c) => c.toString().length > 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
markSyncTime() {
|
||||||
|
set({ lastSyncTime: Date.now(), lastProvider: get().provider });
|
||||||
|
},
|
||||||
|
|
||||||
|
export() {
|
||||||
|
const state = getLocalAppState();
|
||||||
|
const fileName = `Backup-${new Date().toLocaleString()}.json`;
|
||||||
|
downloadAs(JSON.stringify(state), fileName);
|
||||||
|
},
|
||||||
|
|
||||||
|
async import() {
|
||||||
|
const rawContent = await readFromFile();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const remoteState = JSON.parse(rawContent) as AppState;
|
||||||
|
const localState = getLocalAppState();
|
||||||
|
mergeAppState(localState, remoteState);
|
||||||
|
setLocalAppState(localState);
|
||||||
|
location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[Import]", e);
|
||||||
|
showToast(Locale.Settings.Sync.ImportFailed);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getClient() {
|
||||||
|
const provider = get().provider;
|
||||||
|
const client = createSyncClient(provider, get());
|
||||||
|
return client;
|
||||||
|
},
|
||||||
|
|
||||||
|
async sync() {
|
||||||
|
const localState = getLocalAppState();
|
||||||
|
const provider = get().provider;
|
||||||
|
const config = get()[provider];
|
||||||
|
const client = this.getClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const remoteState = JSON.parse(
|
||||||
|
await client.get(config.username),
|
||||||
|
) as AppState;
|
||||||
|
mergeAppState(localState, remoteState);
|
||||||
|
setLocalAppState(localState);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[Sync] failed to get remoate state", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.set(config.username, JSON.stringify(localState));
|
||||||
|
|
||||||
|
this.markSyncTime();
|
||||||
|
},
|
||||||
|
|
||||||
|
async check() {
|
||||||
|
const client = this.getClient();
|
||||||
|
return await client.check();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: StoreKey.Sync,
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,24 +1,7 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
|
import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
|
||||||
import { api } from "../client/api";
|
import { api } from "../client/api";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
|
import { createPersistStore } from "../utils/store";
|
||||||
export interface UpdateStore {
|
|
||||||
versionType: "date" | "tag";
|
|
||||||
lastUpdate: number;
|
|
||||||
version: string;
|
|
||||||
remoteVersion: string;
|
|
||||||
|
|
||||||
used?: number;
|
|
||||||
subscription?: number;
|
|
||||||
lastUpdateUsage: number;
|
|
||||||
|
|
||||||
getLatestVersion: (force?: boolean) => Promise<void>;
|
|
||||||
updateUsage: (force?: boolean) => Promise<void>;
|
|
||||||
|
|
||||||
formatVersion: (version: string) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
|
@ -35,7 +18,9 @@ function formatVersionDate(t: string) {
|
||||||
].join("");
|
].join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVersion(type: "date" | "tag") {
|
type VersionType = "date" | "tag";
|
||||||
|
|
||||||
|
async function getVersion(type: VersionType) {
|
||||||
if (type === "date") {
|
if (type === "date") {
|
||||||
const data = (await (await fetch(FETCH_COMMIT_URL)).json()) as {
|
const data = (await (await fetch(FETCH_COMMIT_URL)).json()) as {
|
||||||
commit: {
|
commit: {
|
||||||
|
@ -55,75 +40,76 @@ async function getVersion(type: "date" | "tag") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateStore = create<UpdateStore>()(
|
export const useUpdateStore = createPersistStore(
|
||||||
persist(
|
{
|
||||||
(set, get) => ({
|
versionType: "tag" as VersionType,
|
||||||
versionType: "tag",
|
lastUpdate: 0,
|
||||||
lastUpdate: 0,
|
version: "unknown",
|
||||||
version: "unknown",
|
remoteVersion: "",
|
||||||
remoteVersion: "",
|
used: 0,
|
||||||
|
subscription: 0,
|
||||||
|
|
||||||
lastUpdateUsage: 0,
|
lastUpdateUsage: 0,
|
||||||
|
},
|
||||||
formatVersion(version: string) {
|
(set, get) => ({
|
||||||
if (get().versionType === "date") {
|
formatVersion(version: string) {
|
||||||
version = formatVersionDate(version);
|
if (get().versionType === "date") {
|
||||||
}
|
version = formatVersionDate(version);
|
||||||
return version;
|
}
|
||||||
},
|
return version;
|
||||||
|
|
||||||
async getLatestVersion(force = false) {
|
|
||||||
const versionType = get().versionType;
|
|
||||||
let version =
|
|
||||||
versionType === "date"
|
|
||||||
? getClientConfig()?.commitDate
|
|
||||||
: getClientConfig()?.version;
|
|
||||||
|
|
||||||
set(() => ({ version }));
|
|
||||||
|
|
||||||
const shouldCheck = Date.now() - get().lastUpdate > 2 * 60 * ONE_MINUTE;
|
|
||||||
if (!force && !shouldCheck) return;
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
lastUpdate: Date.now(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const remoteId = await getVersion(versionType);
|
|
||||||
set(() => ({
|
|
||||||
remoteVersion: remoteId,
|
|
||||||
}));
|
|
||||||
console.log("[Got Upstream] ", remoteId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Fetch Upstream Commit Id]", error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async updateUsage(force = false) {
|
|
||||||
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
|
|
||||||
if (!overOneMinute && !force) return;
|
|
||||||
|
|
||||||
set(() => ({
|
|
||||||
lastUpdateUsage: Date.now(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const usage = await api.llm.usage();
|
|
||||||
|
|
||||||
if (usage) {
|
|
||||||
set(() => ({
|
|
||||||
used: usage.used,
|
|
||||||
subscription: usage.total,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error((e as Error).message);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: StoreKey.Update,
|
|
||||||
version: 1,
|
|
||||||
},
|
},
|
||||||
),
|
|
||||||
|
async getLatestVersion(force = false) {
|
||||||
|
const versionType = get().versionType;
|
||||||
|
let version =
|
||||||
|
versionType === "date"
|
||||||
|
? getClientConfig()?.commitDate
|
||||||
|
: getClientConfig()?.version;
|
||||||
|
|
||||||
|
set(() => ({ version }));
|
||||||
|
|
||||||
|
const shouldCheck = Date.now() - get().lastUpdate > 2 * 60 * ONE_MINUTE;
|
||||||
|
if (!force && !shouldCheck) return;
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
lastUpdate: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const remoteId = await getVersion(versionType);
|
||||||
|
set(() => ({
|
||||||
|
remoteVersion: remoteId,
|
||||||
|
}));
|
||||||
|
console.log("[Got Upstream] ", remoteId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Fetch Upstream Commit Id]", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUsage(force = false) {
|
||||||
|
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
|
||||||
|
if (!overOneMinute && !force) return;
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
lastUpdateUsage: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const usage = await api.llm.usage();
|
||||||
|
|
||||||
|
if (usage) {
|
||||||
|
set(() => ({
|
||||||
|
used: usage.used,
|
||||||
|
subscription: usage.total,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error((e as Error).message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: StoreKey.Update,
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function deepClone<T>(obj: T) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { createWebDavClient } from "./webdav";
|
||||||
|
import { createUpstashClient } from "./upstash";
|
||||||
|
|
||||||
|
export enum ProviderType {
|
||||||
|
WebDAV = "webdav",
|
||||||
|
UpStash = "upstash",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SyncClients = {
|
||||||
|
[ProviderType.UpStash]: createUpstashClient,
|
||||||
|
[ProviderType.WebDAV]: createWebDavClient,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type SyncClientConfig = {
|
||||||
|
[K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends (
|
||||||
|
_: infer C,
|
||||||
|
) => any
|
||||||
|
? C
|
||||||
|
: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SyncClient = {
|
||||||
|
get: (key: string) => Promise<string>;
|
||||||
|
set: (key: string, value: string) => Promise<void>;
|
||||||
|
check: () => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createSyncClient<T extends ProviderType>(
|
||||||
|
provider: T,
|
||||||
|
config: SyncClientConfig[T],
|
||||||
|
): SyncClient {
|
||||||
|
return SyncClients[provider](config as any) as any;
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { SyncStore } from "@/app/store/sync";
|
||||||
|
|
||||||
|
export type UpstashConfig = SyncStore["upstash"];
|
||||||
|
export type UpStashClient = ReturnType<typeof createUpstashClient>;
|
||||||
|
|
||||||
|
export function createUpstashClient(config: UpstashConfig) {
|
||||||
|
return {
|
||||||
|
async check() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async get() {
|
||||||
|
throw Error("[Sync] not implemented");
|
||||||
|
},
|
||||||
|
|
||||||
|
async set() {
|
||||||
|
throw Error("[Sync] not implemented");
|
||||||
|
},
|
||||||
|
|
||||||
|
headers() {
|
||||||
|
return {
|
||||||
|
Authorization: `Basic ${config.apiKey}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
path(path: string) {
|
||||||
|
let url = config.endpoint;
|
||||||
|
|
||||||
|
if (!url.endsWith("/")) {
|
||||||
|
url += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.startsWith("/")) {
|
||||||
|
path = path.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url + path;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { STORAGE_KEY } from "@/app/constant";
|
||||||
|
import { SyncStore } from "@/app/store/sync";
|
||||||
|
import { corsFetch } from "../cors";
|
||||||
|
|
||||||
|
export type WebDAVConfig = SyncStore["webdav"];
|
||||||
|
export type WebDavClient = ReturnType<typeof createWebDavClient>;
|
||||||
|
|
||||||
|
export function createWebDavClient(store: SyncStore) {
|
||||||
|
const folder = STORAGE_KEY;
|
||||||
|
const fileName = `${folder}/backup.json`;
|
||||||
|
const config = store.webdav;
|
||||||
|
const proxyUrl =
|
||||||
|
store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
async check() {
|
||||||
|
try {
|
||||||
|
const res = await corsFetch(this.path(folder), {
|
||||||
|
method: "MKCOL",
|
||||||
|
headers: this.headers(),
|
||||||
|
proxyUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[WebDav] check", res.status, res.statusText);
|
||||||
|
|
||||||
|
return [201, 200, 404].includes(res.status);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[WebDav] failed to check", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async get(key: string) {
|
||||||
|
const res = await corsFetch(this.path(fileName), {
|
||||||
|
method: "GET",
|
||||||
|
headers: this.headers(),
|
||||||
|
proxyUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[WebDav] get key = ", key, res.status, res.statusText);
|
||||||
|
|
||||||
|
return await res.text();
|
||||||
|
},
|
||||||
|
|
||||||
|
async set(key: string, value: string) {
|
||||||
|
const res = await corsFetch(this.path(fileName), {
|
||||||
|
method: "PUT",
|
||||||
|
headers: this.headers(),
|
||||||
|
body: value,
|
||||||
|
proxyUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[WebDav] set key = ", key, res.status, res.statusText);
|
||||||
|
},
|
||||||
|
|
||||||
|
headers() {
|
||||||
|
const auth = btoa(config.username + ":" + config.password);
|
||||||
|
|
||||||
|
return {
|
||||||
|
authorization: `Basic ${auth}`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
path(path: string) {
|
||||||
|
let url = config.endpoint;
|
||||||
|
|
||||||
|
if (!url.endsWith("/")) {
|
||||||
|
url += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.startsWith("/")) {
|
||||||
|
path = path.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url + path;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { getClientConfig } from "../config/client";
|
||||||
|
import { ApiPath, DEFAULT_CORS_HOST } from "../constant";
|
||||||
|
|
||||||
|
export function corsPath(path: string) {
|
||||||
|
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_CORS_HOST}` : "";
|
||||||
|
|
||||||
|
if (!path.startsWith("/")) {
|
||||||
|
path = "/" + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.endsWith("/")) {
|
||||||
|
path += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseUrl}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function corsFetch(
|
||||||
|
url: string,
|
||||||
|
options: RequestInit & {
|
||||||
|
proxyUrl?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (!url.startsWith("http")) {
|
||||||
|
throw Error("[CORS Fetch] url must starts with http/https");
|
||||||
|
}
|
||||||
|
|
||||||
|
let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors);
|
||||||
|
if (!proxyUrl.endsWith("/")) {
|
||||||
|
proxyUrl += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
url = url.replace("://", "/");
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
...options,
|
||||||
|
method: "POST",
|
||||||
|
headers: options.method
|
||||||
|
? {
|
||||||
|
...options.headers,
|
||||||
|
method: options.method,
|
||||||
|
}
|
||||||
|
: options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const corsUrl = proxyUrl + url;
|
||||||
|
console.info("[CORS] target = ", corsUrl);
|
||||||
|
|
||||||
|
return fetch(corsUrl, corsOptions);
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import { Updater } from "../typing";
|
||||||
|
import { deepClone } from "./clone";
|
||||||
|
|
||||||
|
type SecondParam<T> = T extends (
|
||||||
|
_f: infer _F,
|
||||||
|
_s: infer S,
|
||||||
|
...args: infer _U
|
||||||
|
) => any
|
||||||
|
? S
|
||||||
|
: never;
|
||||||
|
|
||||||
|
type MakeUpdater<T> = {
|
||||||
|
lastUpdateTime: number;
|
||||||
|
|
||||||
|
markUpdate: () => void;
|
||||||
|
update: Updater<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetStoreState<T> = (
|
||||||
|
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
|
||||||
|
replace?: boolean | undefined,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export function createPersistStore<T, M>(
|
||||||
|
defaultState: T,
|
||||||
|
methods: (
|
||||||
|
set: SetStoreState<T & MakeUpdater<T>>,
|
||||||
|
get: () => T & MakeUpdater<T>,
|
||||||
|
) => M,
|
||||||
|
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
|
||||||
|
) {
|
||||||
|
return create<T & M & MakeUpdater<T>>()(
|
||||||
|
persist((set, get) => {
|
||||||
|
return {
|
||||||
|
...defaultState,
|
||||||
|
...methods(set as any, get),
|
||||||
|
|
||||||
|
lastUpdateTime: 0,
|
||||||
|
markUpdate() {
|
||||||
|
set({ lastUpdateTime: Date.now() } as Partial<
|
||||||
|
T & M & MakeUpdater<T>
|
||||||
|
>);
|
||||||
|
},
|
||||||
|
update(updater) {
|
||||||
|
const state = deepClone(get());
|
||||||
|
updater(state);
|
||||||
|
get().markUpdate();
|
||||||
|
set(state);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, persistOptions),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
import {
|
||||||
|
ChatSession,
|
||||||
|
useAccessStore,
|
||||||
|
useAppConfig,
|
||||||
|
useChatStore,
|
||||||
|
} from "../store";
|
||||||
|
import { useMaskStore } from "../store/mask";
|
||||||
|
import { usePromptStore } from "../store/prompt";
|
||||||
|
import { StoreKey } from "../constant";
|
||||||
|
import { merge } from "./merge";
|
||||||
|
|
||||||
|
type NonFunctionKeys<T> = {
|
||||||
|
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
|
||||||
|
}[keyof T];
|
||||||
|
type NonFunctionFields<T> = Pick<T, NonFunctionKeys<T>>;
|
||||||
|
|
||||||
|
export function getNonFunctionFileds<T extends object>(obj: T) {
|
||||||
|
const ret: any = {};
|
||||||
|
|
||||||
|
Object.entries(obj).map(([k, v]) => {
|
||||||
|
if (typeof v !== "function") {
|
||||||
|
ret[k] = v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret as NonFunctionFields<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetStoreState<T> = T extends { getState: () => infer U }
|
||||||
|
? NonFunctionFields<U>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
const LocalStateSetters = {
|
||||||
|
[StoreKey.Chat]: useChatStore.setState,
|
||||||
|
[StoreKey.Access]: useAccessStore.setState,
|
||||||
|
[StoreKey.Config]: useAppConfig.setState,
|
||||||
|
[StoreKey.Mask]: useMaskStore.setState,
|
||||||
|
[StoreKey.Prompt]: usePromptStore.setState,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const LocalStateGetters = {
|
||||||
|
[StoreKey.Chat]: () => getNonFunctionFileds(useChatStore.getState()),
|
||||||
|
[StoreKey.Access]: () => getNonFunctionFileds(useAccessStore.getState()),
|
||||||
|
[StoreKey.Config]: () => getNonFunctionFileds(useAppConfig.getState()),
|
||||||
|
[StoreKey.Mask]: () => getNonFunctionFileds(useMaskStore.getState()),
|
||||||
|
[StoreKey.Prompt]: () => getNonFunctionFileds(usePromptStore.getState()),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AppState = {
|
||||||
|
[k in keyof typeof LocalStateGetters]: ReturnType<
|
||||||
|
(typeof LocalStateGetters)[k]
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Merger<T extends keyof AppState, U = AppState[T]> = (
|
||||||
|
localState: U,
|
||||||
|
remoteState: U,
|
||||||
|
) => U;
|
||||||
|
|
||||||
|
type StateMerger = {
|
||||||
|
[K in keyof AppState]: Merger<K>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// we merge remote state to local state
|
||||||
|
const MergeStates: StateMerger = {
|
||||||
|
[StoreKey.Chat]: (localState, remoteState) => {
|
||||||
|
// merge sessions
|
||||||
|
const localSessions: Record<string, ChatSession> = {};
|
||||||
|
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
||||||
|
|
||||||
|
remoteState.sessions.forEach((remoteSession) => {
|
||||||
|
const localSession = localSessions[remoteSession.id];
|
||||||
|
if (!localSession) {
|
||||||
|
// if remote session is new, just merge it
|
||||||
|
localState.sessions.push(remoteSession);
|
||||||
|
} else {
|
||||||
|
// if both have the same session id, merge the messages
|
||||||
|
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
||||||
|
remoteSession.messages.forEach((m) => {
|
||||||
|
if (!localMessageIds.has(m.id)) {
|
||||||
|
localSession.messages.push(m);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort local messages with date field in asc order
|
||||||
|
localSession.messages.sort(
|
||||||
|
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort local sessions with date field in desc order
|
||||||
|
localState.sessions.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return localState;
|
||||||
|
},
|
||||||
|
[StoreKey.Prompt]: (localState, remoteState) => {
|
||||||
|
localState.prompts = {
|
||||||
|
...remoteState.prompts,
|
||||||
|
...localState.prompts,
|
||||||
|
};
|
||||||
|
return localState;
|
||||||
|
},
|
||||||
|
[StoreKey.Mask]: (localState, remoteState) => {
|
||||||
|
localState.masks = {
|
||||||
|
...remoteState.masks,
|
||||||
|
...localState.masks,
|
||||||
|
};
|
||||||
|
return localState;
|
||||||
|
},
|
||||||
|
[StoreKey.Config]: mergeWithUpdate<AppState[StoreKey.Config]>,
|
||||||
|
[StoreKey.Access]: mergeWithUpdate<AppState[StoreKey.Access]>,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLocalAppState() {
|
||||||
|
const appState = Object.fromEntries(
|
||||||
|
Object.entries(LocalStateGetters).map(([key, getter]) => {
|
||||||
|
return [key, getter()];
|
||||||
|
}),
|
||||||
|
) as AppState;
|
||||||
|
|
||||||
|
return appState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalAppState(appState: AppState) {
|
||||||
|
Object.entries(LocalStateSetters).forEach(([key, setter]) => {
|
||||||
|
setter(appState[key as keyof AppState]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeAppState(localState: AppState, remoteState: AppState) {
|
||||||
|
Object.keys(localState).forEach(<T extends keyof AppState>(k: string) => {
|
||||||
|
const key = k as T;
|
||||||
|
const localStoreState = localState[key];
|
||||||
|
const remoteStoreState = remoteState[key];
|
||||||
|
MergeStates[key](localStoreState, remoteStoreState);
|
||||||
|
});
|
||||||
|
|
||||||
|
return localState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge state with `lastUpdateTime`, older state will be override
|
||||||
|
*/
|
||||||
|
export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
|
||||||
|
localState: T,
|
||||||
|
remoteState: T,
|
||||||
|
) {
|
||||||
|
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
||||||
|
const remoteUpdateTime = localState.lastUpdateTime ?? 1;
|
||||||
|
|
||||||
|
if (localUpdateTime < remoteUpdateTime) {
|
||||||
|
merge(remoteState, localState);
|
||||||
|
return { ...remoteState };
|
||||||
|
} else {
|
||||||
|
merge(localState, remoteState);
|
||||||
|
return { ...localState };
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
||||||
7. In "Build Settings", choose the "Framework presets" option and select "Next.js".
|
7. In "Build Settings", choose the "Framework presets" option and select "Next.js".
|
||||||
8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command:
|
8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command:
|
||||||
```
|
```
|
||||||
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
|
npx @cloudflare/next-on-pages --experimental-minify
|
||||||
```
|
```
|
||||||
9. For "Build output directory", use the default value and do not modify it.
|
9. For "Build output directory", use the default value and do not modify it.
|
||||||
10. Do not modify "Root Directory".
|
10. Do not modify "Root Directory".
|
||||||
|
@ -35,4 +35,4 @@ Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
||||||
14. Go to "Build settings", "Functions", and find "Compatibility flags".
|
14. Go to "Build settings", "Functions", and find "Compatibility flags".
|
||||||
15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag".
|
15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag".
|
||||||
16. Go to "Deployments" and click "Retry deployment".
|
16. Go to "Deployments" and click "Retry deployment".
|
||||||
17. Enjoy.
|
17. Enjoy.
|
||||||
|
|
|
@ -35,27 +35,29 @@ const nextConfig = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CorsHeaders = [
|
||||||
|
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||||
|
{ key: "Access-Control-Allow-Origin", value: "*" },
|
||||||
|
{
|
||||||
|
key: "Access-Control-Allow-Methods",
|
||||||
|
value: "*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Access-Control-Allow-Headers",
|
||||||
|
value: "*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "Access-Control-Max-Age",
|
||||||
|
value: "86400",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (mode !== "export") {
|
if (mode !== "export") {
|
||||||
nextConfig.headers = async () => {
|
nextConfig.headers = async () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/api/:path*",
|
source: "/api/:path*",
|
||||||
headers: [
|
headers: CorsHeaders,
|
||||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
|
||||||
{ key: "Access-Control-Allow-Origin", value: "*" },
|
|
||||||
{
|
|
||||||
key: "Access-Control-Allow-Methods",
|
|
||||||
value: "*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "Access-Control-Allow-Headers",
|
|
||||||
value: "*",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "Access-Control-Max-Age",
|
|
||||||
value: "86400",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -76,15 +78,6 @@ if (mode !== "export") {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const apiUrl = process.env.API_URL;
|
|
||||||
if (apiUrl) {
|
|
||||||
console.log("[Next] using api url ", apiUrl);
|
|
||||||
ret.push({
|
|
||||||
source: "/api/:path*",
|
|
||||||
destination: `${apiUrl}/:path*`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
beforeFiles: ret,
|
beforeFiles: ret,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"@hello-pangea/dnd": "^16.3.0",
|
"@hello-pangea/dnd": "^16.3.0",
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
"@vercel/analytics": "^0.1.11",
|
"@vercel/analytics": "^0.1.11",
|
||||||
"emoji-picker-react": "^4.4.7",
|
"emoji-picker-react": "^4.5.1",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"mermaid": "^10.3.1",
|
"mermaid": "^10.3.1",
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.14.1",
|
"react-router-dom": "^6.15.0",
|
||||||
"rehype-highlight": "^6.0.0",
|
"rehype-highlight": "^6.0.0",
|
||||||
"rehype-katex": "^6.0.3",
|
"rehype-katex": "^6.0.3",
|
||||||
"remark-breaks": "^3.0.2",
|
"remark-breaks": "^3.0.2",
|
||||||
|
@ -49,14 +49,14 @@
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
"@types/spark-md5": "^3.0.2",
|
"@types/spark-md5": "^3.0.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.44.0",
|
"eslint": "^8.49.0",
|
||||||
"eslint-config-next": "13.4.19",
|
"eslint-config-next": "13.4.19",
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"husky": "^8.0.0",
|
"husky": "^8.0.0",
|
||||||
"lint-staged": "^13.2.2",
|
"lint-staged": "^13.2.2",
|
||||||
"prettier": "^3.0.2",
|
"prettier": "^3.0.2",
|
||||||
"typescript": "4.9.5",
|
"typescript": "5.2.2",
|
||||||
"webpack": "^5.88.1"
|
"webpack": "^5.88.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "ChatGPT Next Web",
|
"productName": "ChatGPT Next Web",
|
||||||
"version": "2.9.5"
|
"version": "2.9.6"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
|
134
yarn.lock
134
yarn.lock
|
@ -1012,15 +1012,15 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint-visitor-keys "^3.3.0"
|
eslint-visitor-keys "^3.3.0"
|
||||||
|
|
||||||
"@eslint-community/regexpp@^4.4.0":
|
"@eslint-community/regexpp@^4.6.1":
|
||||||
version "4.5.0"
|
version "4.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724"
|
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005"
|
||||||
integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==
|
integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==
|
||||||
|
|
||||||
"@eslint/eslintrc@^2.1.0":
|
"@eslint/eslintrc@^2.1.2":
|
||||||
version "2.1.0"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d"
|
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396"
|
||||||
integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==
|
integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv "^6.12.4"
|
ajv "^6.12.4"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
|
@ -1032,10 +1032,10 @@
|
||||||
minimatch "^3.1.2"
|
minimatch "^3.1.2"
|
||||||
strip-json-comments "^3.1.1"
|
strip-json-comments "^3.1.1"
|
||||||
|
|
||||||
"@eslint/js@8.44.0":
|
"@eslint/js@8.49.0":
|
||||||
version "8.44.0"
|
version "8.49.0"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af"
|
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333"
|
||||||
integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==
|
integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==
|
||||||
|
|
||||||
"@fortaine/fetch-event-source@^3.0.6":
|
"@fortaine/fetch-event-source@^3.0.6":
|
||||||
version "3.0.6"
|
version "3.0.6"
|
||||||
|
@ -1055,10 +1055,10 @@
|
||||||
redux "^4.2.1"
|
redux "^4.2.1"
|
||||||
use-memo-one "^1.1.3"
|
use-memo-one "^1.1.3"
|
||||||
|
|
||||||
"@humanwhocodes/config-array@^0.11.10":
|
"@humanwhocodes/config-array@^0.11.11":
|
||||||
version "0.11.10"
|
version "0.11.11"
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
|
||||||
integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==
|
integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@humanwhocodes/object-schema" "^1.2.1"
|
"@humanwhocodes/object-schema" "^1.2.1"
|
||||||
debug "^4.1.1"
|
debug "^4.1.1"
|
||||||
|
@ -1221,10 +1221,10 @@
|
||||||
tiny-glob "^0.2.9"
|
tiny-glob "^0.2.9"
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@remix-run/router@1.7.1":
|
"@remix-run/router@1.8.0":
|
||||||
version "1.7.1"
|
version "1.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498"
|
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
|
||||||
integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==
|
integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==
|
||||||
|
|
||||||
"@rushstack/eslint-patch@^1.1.3":
|
"@rushstack/eslint-patch@^1.1.3":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
|
@ -1779,7 +1779,7 @@ ajv-keywords@^3.5.2:
|
||||||
resolved "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
resolved "https://registry.npmmirror.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
|
||||||
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
|
||||||
|
|
||||||
ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5:
|
ajv@^6.12.4, ajv@^6.12.5:
|
||||||
version "6.12.6"
|
version "6.12.6"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||||
|
@ -2762,10 +2762,10 @@ elkjs@^0.8.2:
|
||||||
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
||||||
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
|
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
|
||||||
|
|
||||||
emoji-picker-react@^4.4.7:
|
emoji-picker-react@^4.5.1:
|
||||||
version "4.4.8"
|
version "4.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.4.8.tgz#cd18e942720d0d01e3d488a008f5e79aa315ec87"
|
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.1.tgz#341f27dc86ad09340a316e0632484fcb9aff7195"
|
||||||
integrity sha512-5bbj0PCvpjB64PZj31wZ35EoebF2mKoHqEEx9u2ZLghx7sGoD1MgyDhse851rqROypjhmK9IUY15QBa7mCLP0g==
|
integrity sha512-zpm0ui0TWkXZDUIevyNM0rC9Jyqc08RvVXH0KgsbSkDr+VgMQmYLu6UeI4SIWMZKsKMjQwujPpncRCFlEeykjw==
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx "^1.2.1"
|
clsx "^1.2.1"
|
||||||
|
|
||||||
|
@ -3050,40 +3050,40 @@ eslint-scope@5.1.1:
|
||||||
esrecurse "^4.3.0"
|
esrecurse "^4.3.0"
|
||||||
estraverse "^4.1.1"
|
estraverse "^4.1.1"
|
||||||
|
|
||||||
eslint-scope@^7.2.0:
|
eslint-scope@^7.2.2:
|
||||||
version "7.2.0"
|
version "7.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
|
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
|
||||||
integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
|
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
|
||||||
dependencies:
|
dependencies:
|
||||||
esrecurse "^4.3.0"
|
esrecurse "^4.3.0"
|
||||||
estraverse "^5.2.0"
|
estraverse "^5.2.0"
|
||||||
|
|
||||||
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
|
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
|
||||||
version "3.4.1"
|
version "3.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
|
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
|
||||||
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
|
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
|
||||||
|
|
||||||
eslint@^8.44.0:
|
eslint@^8.49.0:
|
||||||
version "8.44.0"
|
version "8.49.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500"
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42"
|
||||||
integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==
|
integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@eslint-community/eslint-utils" "^4.2.0"
|
"@eslint-community/eslint-utils" "^4.2.0"
|
||||||
"@eslint-community/regexpp" "^4.4.0"
|
"@eslint-community/regexpp" "^4.6.1"
|
||||||
"@eslint/eslintrc" "^2.1.0"
|
"@eslint/eslintrc" "^2.1.2"
|
||||||
"@eslint/js" "8.44.0"
|
"@eslint/js" "8.49.0"
|
||||||
"@humanwhocodes/config-array" "^0.11.10"
|
"@humanwhocodes/config-array" "^0.11.11"
|
||||||
"@humanwhocodes/module-importer" "^1.0.1"
|
"@humanwhocodes/module-importer" "^1.0.1"
|
||||||
"@nodelib/fs.walk" "^1.2.8"
|
"@nodelib/fs.walk" "^1.2.8"
|
||||||
ajv "^6.10.0"
|
ajv "^6.12.4"
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
cross-spawn "^7.0.2"
|
cross-spawn "^7.0.2"
|
||||||
debug "^4.3.2"
|
debug "^4.3.2"
|
||||||
doctrine "^3.0.0"
|
doctrine "^3.0.0"
|
||||||
escape-string-regexp "^4.0.0"
|
escape-string-regexp "^4.0.0"
|
||||||
eslint-scope "^7.2.0"
|
eslint-scope "^7.2.2"
|
||||||
eslint-visitor-keys "^3.4.1"
|
eslint-visitor-keys "^3.4.3"
|
||||||
espree "^9.6.0"
|
espree "^9.6.1"
|
||||||
esquery "^1.4.2"
|
esquery "^1.4.2"
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
fast-deep-equal "^3.1.3"
|
fast-deep-equal "^3.1.3"
|
||||||
|
@ -3093,7 +3093,6 @@ eslint@^8.44.0:
|
||||||
globals "^13.19.0"
|
globals "^13.19.0"
|
||||||
graphemer "^1.4.0"
|
graphemer "^1.4.0"
|
||||||
ignore "^5.2.0"
|
ignore "^5.2.0"
|
||||||
import-fresh "^3.0.0"
|
|
||||||
imurmurhash "^0.1.4"
|
imurmurhash "^0.1.4"
|
||||||
is-glob "^4.0.0"
|
is-glob "^4.0.0"
|
||||||
is-path-inside "^3.0.3"
|
is-path-inside "^3.0.3"
|
||||||
|
@ -3105,13 +3104,12 @@ eslint@^8.44.0:
|
||||||
natural-compare "^1.4.0"
|
natural-compare "^1.4.0"
|
||||||
optionator "^0.9.3"
|
optionator "^0.9.3"
|
||||||
strip-ansi "^6.0.1"
|
strip-ansi "^6.0.1"
|
||||||
strip-json-comments "^3.1.0"
|
|
||||||
text-table "^0.2.0"
|
text-table "^0.2.0"
|
||||||
|
|
||||||
espree@^9.6.0:
|
espree@^9.6.0, espree@^9.6.1:
|
||||||
version "9.6.0"
|
version "9.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f"
|
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
|
||||||
integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==
|
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn "^8.9.0"
|
acorn "^8.9.0"
|
||||||
acorn-jsx "^5.3.2"
|
acorn-jsx "^5.3.2"
|
||||||
|
@ -3635,7 +3633,7 @@ immutable@^4.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be"
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be"
|
||||||
integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==
|
integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==
|
||||||
|
|
||||||
import-fresh@^3.0.0, import-fresh@^3.2.1:
|
import-fresh@^3.2.1:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||||
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
|
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
|
||||||
|
@ -5092,20 +5090,20 @@ react-redux@^8.1.1:
|
||||||
react-is "^18.0.0"
|
react-is "^18.0.0"
|
||||||
use-sync-external-store "^1.0.0"
|
use-sync-external-store "^1.0.0"
|
||||||
|
|
||||||
react-router-dom@^6.14.1:
|
react-router-dom@^6.15.0:
|
||||||
version "6.14.1"
|
version "6.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f"
|
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.15.0.tgz#6da7db61e56797266fbbef0d5e324d6ac443ee40"
|
||||||
integrity sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw==
|
integrity sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@remix-run/router" "1.7.1"
|
"@remix-run/router" "1.8.0"
|
||||||
react-router "6.14.1"
|
react-router "6.15.0"
|
||||||
|
|
||||||
react-router@6.14.1:
|
react-router@6.15.0:
|
||||||
version "6.14.1"
|
version "6.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810"
|
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8"
|
||||||
integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g==
|
integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@remix-run/router" "1.7.1"
|
"@remix-run/router" "1.8.0"
|
||||||
|
|
||||||
react@^18.2.0:
|
react@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
|
@ -5588,7 +5586,7 @@ strip-final-newline@^3.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
|
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
|
||||||
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
|
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
|
||||||
|
|
||||||
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
|
strip-json-comments@^3.1.1:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||||
|
@ -5786,10 +5784,10 @@ typed-array-length@^1.0.4:
|
||||||
for-each "^0.3.3"
|
for-each "^0.3.3"
|
||||||
is-typed-array "^1.1.9"
|
is-typed-array "^1.1.9"
|
||||||
|
|
||||||
typescript@4.9.5:
|
typescript@5.2.2:
|
||||||
version "4.9.5"
|
version "5.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
|
||||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
|
||||||
|
|
||||||
unbox-primitive@^1.0.2:
|
unbox-primitive@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
|
|
Loading…
Reference in New Issue