mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-16 05:53:42 +08:00
Merge branch 'Yidadaa:main' into main
This commit is contained in:
@@ -15,6 +15,7 @@ export function AuthPage() {
|
||||
const access = useAccessStore();
|
||||
|
||||
const goHome = () => navigate(Path.Home);
|
||||
const resetAccessCode = () => access.updateCode(""); // Reset access code to empty string
|
||||
|
||||
useEffect(() => {
|
||||
if (getClientConfig()?.isApp) {
|
||||
@@ -48,7 +49,10 @@ export function AuthPage() {
|
||||
type="primary"
|
||||
onClick={goHome}
|
||||
/>
|
||||
<IconButton text={Locale.Auth.Later} onClick={goHome} />
|
||||
<IconButton text={Locale.Auth.Later} onClick={() => {
|
||||
resetAccessCode();
|
||||
goHome();
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
MAX_RENDER_MSG_COUNT,
|
||||
Path,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
UNFINISHED_INPUT,
|
||||
} from "../constant";
|
||||
import { Avatar } from "./emoji";
|
||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||
@@ -935,7 +936,8 @@ function _Chat() {
|
||||
|
||||
const isTouchTopEdge = e.scrollTop <= 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 nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
|
||||
@@ -1013,6 +1015,23 @@ function _Chat() {
|
||||
// edit / insert message modal
|
||||
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 (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<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 { ISSUE_URL } from "../constant";
|
||||
import Locale from "../locales";
|
||||
import { downloadAs } from "../utils";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { useSyncStore } from "../store/sync";
|
||||
|
||||
interface IErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
@@ -26,10 +26,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
|
||||
clearAndSaveData() {
|
||||
try {
|
||||
downloadAs(
|
||||
JSON.stringify(localStorage),
|
||||
"chatgpt-next-web-snapshot.json",
|
||||
);
|
||||
useSyncStore.getState().export();
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
|
||||
@@ -410,7 +410,7 @@ export function MaskPage() {
|
||||
const closeMaskModal = () => setEditingMaskId(undefined);
|
||||
|
||||
const downloadAll = () => {
|
||||
downloadAs(JSON.stringify(masks), FileName.Masks);
|
||||
downloadAs(JSON.stringify(masks.filter((v) => !v.builtin)), FileName.Masks);
|
||||
};
|
||||
|
||||
const importFromFile = () => {
|
||||
@@ -452,11 +452,13 @@ export function MaskPage() {
|
||||
icon={<DownloadIcon />}
|
||||
bordered
|
||||
onClick={downloadAll}
|
||||
text={Locale.UI.Export}
|
||||
/>
|
||||
</div>
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<UploadIcon />}
|
||||
text={Locale.UI.Import}
|
||||
bordered
|
||||
onClick={() => importFromFile()}
|
||||
/>
|
||||
@@ -604,7 +606,7 @@ export function MaskPage() {
|
||||
<MaskConfig
|
||||
mask={editingMask}
|
||||
updateMask={(updater) =>
|
||||
maskStore.update(editingMaskId!, updater)
|
||||
maskStore.updateMask(editingMaskId!, updater)
|
||||
}
|
||||
readonly={editingMask.builtin}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,15 @@ import ClearIcon from "../icons/clear.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import EditIcon from "../icons/edit.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 {
|
||||
Input,
|
||||
List,
|
||||
@@ -19,6 +28,7 @@ import {
|
||||
Popover,
|
||||
Select,
|
||||
showConfirm,
|
||||
showToast,
|
||||
} from "./ui-lib";
|
||||
import { ModelConfigList } from "./model-config";
|
||||
|
||||
@@ -49,6 +59,8 @@ import { Avatar, AvatarPicker } from "./emoji";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { useSyncStore } from "../store/sync";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { ProviderType } from "../utils/cloud";
|
||||
|
||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
const promptStore = usePromptStore();
|
||||
@@ -75,7 +87,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
readOnly={!prompt.isUser}
|
||||
className={styles["edit-prompt-title"]}
|
||||
onInput={(e) =>
|
||||
promptStore.update(
|
||||
promptStore.updatePrompt(
|
||||
props.id,
|
||||
(prompt) => (prompt.title = e.currentTarget.value),
|
||||
)
|
||||
@@ -87,7 +99,7 @@ function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
className={styles["edit-prompt-content"]}
|
||||
rows={10}
|
||||
onInput={(e) =>
|
||||
promptStore.update(
|
||||
promptStore.updatePrompt(
|
||||
props.id,
|
||||
(prompt) => (prompt.content = e.currentTarget.value),
|
||||
)
|
||||
@@ -127,14 +139,15 @@ function UserPromptModal(props: { onClose?: () => void }) {
|
||||
actions={[
|
||||
<IconButton
|
||||
key="add"
|
||||
onClick={() =>
|
||||
promptStore.add({
|
||||
onClick={() => {
|
||||
const promptId = promptStore.add({
|
||||
id: nanoid(),
|
||||
createdAt: Date.now(),
|
||||
title: "Empty Prompt",
|
||||
content: "Empty Prompt Content",
|
||||
})
|
||||
}
|
||||
});
|
||||
setEditingPromptId(promptId);
|
||||
}}
|
||||
icon={<AddIcon />}
|
||||
bordered
|
||||
text={Locale.Settings.Prompt.Modal.Add}
|
||||
@@ -241,75 +254,262 @@ function DangerItems() {
|
||||
);
|
||||
}
|
||||
|
||||
function SyncItems() {
|
||||
function CheckButton() {
|
||||
const syncStore = useSyncStore();
|
||||
const webdav = syncStore.webDavConfig;
|
||||
|
||||
// not ready: https://github.com/Yidadaa/ChatGPT-Next-Web/issues/920#issuecomment-1609866332
|
||||
return null;
|
||||
const couldCheck = useMemo(() => {
|
||||
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 (
|
||||
<List>
|
||||
<ListItem
|
||||
title={"上次同步:" + new Date().toLocaleString()}
|
||||
subTitle={"20 次对话,100 条消息,200 提示词,20 面具"}
|
||||
<IconButton
|
||||
text="检查可用性"
|
||||
bordered
|
||||
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
|
||||
icon={<ResetIcon />}
|
||||
text="同步"
|
||||
onClick={() => {
|
||||
syncStore.check().then(console.log);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<List>
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.SyncType.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
|
||||
>
|
||||
<select
|
||||
value={syncStore.provider}
|
||||
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
|
||||
title={"本地备份"}
|
||||
subTitle={"20 次对话,100 条消息,200 提示词,20 面具"}
|
||||
></ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.Proxy.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
||||
>
|
||||
<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
|
||||
title={"Web Dav Server"}
|
||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||
>
|
||||
<input
|
||||
value={webdav.server}
|
||||
type="text"
|
||||
placeholder={"https://example.com"}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.server = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{syncStore.provider === ProviderType.WebDAV && (
|
||||
<>
|
||||
<List>
|
||||
<ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
|
||||
<input
|
||||
type="text"
|
||||
value={syncStore.webdav.endpoint}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) =>
|
||||
(config.webdav.endpoint = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem title="Web Dav User Name" subTitle="user name here">
|
||||
<input
|
||||
value={webdav.username}
|
||||
type="text"
|
||||
placeholder={"username"}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.username = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
|
||||
<input
|
||||
type="text"
|
||||
value={syncStore.webdav.username}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) =>
|
||||
(config.webdav.username = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</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">
|
||||
<input
|
||||
value={webdav.password}
|
||||
type="text"
|
||||
placeholder={"password"}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.password = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
{syncStore.provider === ProviderType.UpStash && (
|
||||
<List>
|
||||
<ListItem title={Locale.WIP}></ListItem>
|
||||
</List>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SyncItems() {
|
||||
const syncStore = useSyncStore();
|
||||
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>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
|
||||
<List>
|
||||
<ListItem
|
||||
title={Locale.Settings.Mask.Splash.Title}
|
||||
@@ -710,8 +912,6 @@ export function Settings() {
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
|
||||
<List>
|
||||
<ModelConfigList
|
||||
modelConfig={config.modelConfig}
|
||||
|
||||
Reference in New Issue
Block a user