Compare commits

...

88 Commits

Author SHA1 Message Date
Yifei Zhang
13c1d2fd2b Merge pull request #2946 from H0llyW00dzZ/ClientApp 2023-10-07 23:50:49 +08:00
Yifei Zhang
e35c807216 Update tauri.conf.json 2023-10-07 23:48:50 +08:00
Yifei Zhang
5a2cc6f154 Merge pull request #2927 from lifeeric/MACOS 2023-10-07 23:10:43 +08:00
Yifei Zhang
ef3e8e6fac Merge pull request #2933 from H0llyW00dzZ/authpage 2023-10-07 23:03:56 +08:00
H0llyW00dzZ
b558d1afc6 Feat & Fix "Client App [Notification]"
[+] feat(update.ts): add support for localization in update notifications
[+] fix(update.ts): add missing semicolon in useUpdateStore function
2023-10-04 02:10:26 +07:00
H0llyW00dzZ
ddfd05b008 Fix & Feat Client App [Notification]
[+] fix(update.ts): remove unnecessary notification sending when permission is not granted
[+] feat(update.ts): add notification for already up to date version
2023-10-03 09:12:41 +07:00
H0llyW00dzZ
d2ad01a9ff Client App Fix Issue [Bug] 'export' button does not work #2884
[+] fix(exporter.tsx): add async keyword to download function
[+] feat(exporter.tsx): add support for saving image file using window.__TAURI__ API
[+] feat(global.d.ts): add types for window.__TAURI__ API methods
[+] feat(locales): add translations for download success and failure messages
[+] feat(sync.ts): add support for generating backup file name with date and time
[+] fix(utils.ts): add async keyword to downloadAs function and add support for saving file using window.__TAURI__ API
2023-10-03 08:49:03 +07:00
H0llyW00dzZ
64a17abfe2 Client App [Notification]
[+] feat(global.d.ts): add support for window.__TAURI__.notification methods
[+] feat(update.ts): add notification for new version availability
[+] fix(Cargo.toml): add tauri feature "notification-all" to enable notifications
[+] fix(tauri.conf.json): enable all notification features in tauri configuration
2023-10-03 08:08:11 +07:00
H0llyW00dzZ
04b638aa06 Fix & Refactor UI Page [Auth Page]
[+] fix(auth.tsx): fix conditional rendering of token input field
[+] refactor(auth.tsx): improve code readability by using conditional rendering for token input field
2023-10-01 00:23:19 +07:00
H0llyW00dzZ
31e30906d0 Refactor Locale Indonesia
[+] refactor(id.ts): remove unused import and isApp variable
[+] fix(id.ts): update Unauthorized error message
2023-09-30 22:59:24 +07:00
H0llyW00dzZ
bc00be9065 Feat & Fix UI Page [Auth Page]
[+] feat(auth.tsx): add goChat function to navigate to chat page
[+] fix(auth.tsx): change onClick event from goHome to goChat
2023-09-30 22:33:18 +07:00
H0llyW00dzZ
4a599e986f UI Page [Auth Page]
[+] feat(auth.tsx): add support for resetting access token in resetAccessCode function
[+] fix(auth.tsx): fix formatting issue in resetAccessCode function
[+] feat(locales): add support for sub tips in Auth component for multiple languages :
  - Add sub tips in Arabic locale (ar.ts)
  - Add sub tips in Bengali locale (bn.ts)
  - Add sub tips in Chinese locale (cn.ts)
  - Add sub tips in English locale (en.ts)
  - Add sub tips in Indonesian locale (id.ts)
2023-09-30 22:16:34 +07:00
EricGit
f1ca03e378 [FIXED] now it should detects all macintosh 2023-09-28 13:21:17 -04:00
Eric R
f3d5fc7a84 [FIXED] now the default key should be CMD on MacOS 2023-09-28 06:50:31 -04:00
Eric R
3bfcdf9c41 [ADDED] MacOS detect 2023-09-28 06:10:22 -04:00
Yifei Zhang
144200e315 Merge pull request #2912 from Algorithm5838/Algorithm5838-patch-1 2023-09-27 17:18:15 +08:00
Algorithm5838
398e229c77 Update chat.tsx 2023-09-26 21:49:17 +03:00
Algorithm5838
6a61fe5776 Update chat.tsx 2023-09-26 20:45:09 +03:00
Yifei Zhang
9835206452 Merge pull request #2905 from H0llyW00dzZ/ref#2895 2023-09-26 10:50:49 +08:00
H0llyW00dzZ
70b0580fb7 UI Page [sidebar]
[+] fix(sidebar.tsx): update onClick function to conditionally navigate to different paths based on config.dontShowMaskSplashScreen value

Ref : [Feature] Make the mask selection more streamlined Yidadaa#2895
2023-09-26 04:59:19 +07:00
Yifei Zhang
23eb7732d7 feat: support more http status check for webdav 2023-09-20 17:47:35 +08:00
Yifei Zhang
26e50cefea Update chat.tsx 2023-09-20 02:09:14 +08:00
Yifei Zhang
588e907181 Merge pull request #2873 from Yidadaa/bugfix-0919 2023-09-19 23:42:09 +08:00
Yidadaa
eae7d6260f fix: should not tight border in desktop app 2023-09-19 23:26:52 +08:00
Yifei Zhang
175b4e7f92 Update README_CN.md 2023-09-19 11:04:10 +08:00
Yifei Zhang
b050417ab1 Update tauri.conf.json 2023-09-19 11:03:22 +08:00
Yifei Zhang
37b49400db Update constant.ts 2023-09-19 11:03:03 +08:00
Yifei Zhang
ebcb2e7837 Merge pull request #2867 from Yidadaa/bugfix-0919 2023-09-19 03:34:45 +08:00
Yidadaa
f1e7db6a88 feat: auto fill upstash backup name 2023-09-19 03:33:17 +08:00
Yifei Zhang
2ba0929458 Merge pull request #2866 from Yidadaa/bugfix-0919 2023-09-19 03:20:37 +08:00
Yidadaa
83fed42997 feat: add upstash redis cloud sync 2023-09-19 03:18:34 +08:00
Yifei Zhang
2c4626709c Merge pull request #2865 from Yidadaa/bugfix-0919 2023-09-19 02:25:35 +08:00
Yidadaa
59fbadd9eb fixup 2023-09-19 02:21:31 +08:00
Yidadaa
adb860b464 fix: #2820 try to fix 520 error code 2023-09-19 02:12:43 +08:00
Yifei Zhang
8d8790586d Merge pull request #2864 from Yidadaa/bugfix-0919 2023-09-19 02:02:18 +08:00
Yidadaa
61ca60c550 fix: #2817 min-height for landscape orientation on mobile phone 2023-09-19 01:58:52 +08:00
Yidadaa
d713d01600 feat: close #2848 click drag icon to toggle sidebar width 2023-09-19 01:47:15 +08:00
Yifei Zhang
372ea0f845 Merge pull request #2853 from a6z6/main 2023-09-18 17:33:09 +08:00
Yifei Zhang
61888708f5 Merge pull request #2856 from koho/fix-font 2023-09-18 17:32:56 +08:00
Gerhard Tan
c900459f73 Encode google font url 2023-09-18 10:22:07 +08:00
Amor Zara
2c92f75c86 Update .env.template 2023-09-17 17:32:42 +08:00
Yifei Zhang
3e1514239c Merge pull request #2839 from a6z6/main 2023-09-15 15:17:46 +08:00
Yifei Zhang
0707a1d49a Merge pull request #2840 from Algorithm5838/Algorithm5838-patch-1 2023-09-15 15:17:35 +08:00
Amor Zara
9521f19507 Merge pull request #1 from a6z6/a6z6-patch-1
Update route.ts
2023-09-15 11:23:42 +08:00
Amor Zara
bd69116df2 Update route.ts
Correct typo error and make warning more specific.
2023-09-15 11:21:42 +08:00
Algorithm5838
6535986484 Update markdown.tsx 2023-09-15 06:06:34 +03:00
Yifei Zhang
e03db9c2d5 Merge pull request #2826 from H0llyW00dzZ/language 2023-09-14 10:49:44 +08:00
Yifei Zhang
038790370c Merge pull request #2822 from yhua1998/main 2023-09-13 22:40:03 +08:00
H0llyW00dzZ
261bf0b298 Changes "Nama" -> "User" 2023-09-13 20:49:22 +07:00
H0llyW00dzZ
5a7bdcfe59 [+] Some improvements to the Indonesian language. 2023-09-13 20:17:30 +07:00
H0llyW00dzZ
4f3261b262 [+] Some improvements to the Indonesian language. 2023-09-13 20:05:58 +07:00
yhua1998
48e6087b1b fix: The width of the sidebar changes abruptly by dragging it multiple times over and over again (bouncing) 2023-09-13 16:55:31 +08:00
yhua1998
28103c901d Refactor: sidebar drag 2023-09-13 16:24:21 +08:00
yhua1998
368701610f fix: Width changes abruptly when dragging the sidebar (jumps)
In useRef is non-responsive, we can't get the modified config.sidebarWidth dynamic modification value in handleMouseUp
2023-09-13 13:57:30 +08:00
Yifei Zhang
b589f48aa9 Update tauri.conf.json 2023-09-13 03:01:28 +08:00
Yifei Zhang
74e32efa7d Merge pull request #2819 from Yidadaa/webdav 2023-09-13 02:56:10 +08:00
Yidadaa
dc555b2206 fixup 2023-09-13 02:52:28 +08:00
Yidadaa
859cf6930f fixup 2023-09-13 02:51:57 +08:00
Yidadaa
6f83fbd212 feat: add webdav support 2023-09-13 02:51:02 +08:00
Yifei Zhang
a7be4780ba Merge pull request #2809 from kfear1337/auth-page 2023-09-12 18:14:13 +08:00
B0zal
b5e89d4440 [+] Updated Auth Page
- Made changes to the Auth Page to reset the input field for the access code when the "Later" button is clicked.
This ensures that only expected user is logged by entering access code or entering their OpenAI API Key, mitigating the risk of small bug issue
2023-09-12 07:00:23 +07:00
Yifei Zhang
89bc98d26b Merge pull request #2804 from Yidadaa/dependabot/npm_and_yarn/emoji-picker-react-4.5.1 2023-09-11 20:37:16 +08:00
Yifei Zhang
31a73273ac Merge pull request #2805 from Yidadaa/dependabot/npm_and_yarn/react-router-dom-6.15.0 2023-09-11 20:37:01 +08:00
Yifei Zhang
2a820ea6e7 Merge pull request #2808 from Yidadaa/dependabot/npm_and_yarn/eslint-8.49.0 2023-09-11 20:36:47 +08:00
Yifei Zhang
6484d4f0bd Merge pull request #2807 from Yidadaa/dependabot/npm_and_yarn/typescript-5.2.2 2023-09-11 20:36:33 +08:00
Yifei Zhang
461e48be32 Merge pull request #2799 from kfear1337/CodeQL-Report 2023-09-11 20:35:04 +08:00
dependabot[bot]
ff60ffca3e chore(deps-dev): bump eslint from 8.44.0 to 8.49.0
Bumps [eslint](https://github.com/eslint/eslint) from 8.44.0 to 8.49.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.44.0...v8.49.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:34 +00:00
dependabot[bot]
1bbf310c46 chore(deps-dev): bump typescript from 4.9.5 to 5.2.2
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.2.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.9.5...v5.2.2)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:25 +00:00
dependabot[bot]
8469f448b5 chore(deps): bump react-router-dom from 6.14.1 to 6.15.0
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.14.1 to 6.15.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.15.0/packages/react-router-dom)

---
updated-dependencies:
- dependency-name: react-router-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:32:00 +00:00
dependabot[bot]
e36abc3ac6 chore(deps): bump emoji-picker-react from 4.4.8 to 4.5.1
Bumps [emoji-picker-react](https://github.com/ealush/emoji-picker-react) from 4.4.8 to 4.5.1.
- [Release notes](https://github.com/ealush/emoji-picker-react/releases)
- [Commits](https://github.com/ealush/emoji-picker-react/commits)

---
updated-dependencies:
- dependency-name: emoji-picker-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-11 10:31:50 +00:00
B0zal
605dd72354 [+] CodeQL Report Fix log injection vulnerability in useSyncStore
Severity : High
Sanitize the 'res' object before logging it in the 'check' method of useSyncStore to prevent log injection attacks.
The 'res' object is now sanitized by extracting only the necessary properties ('status', 'statusText', and 'headers') and logging the sanitized object instead.
This ensures that only safe and expected data is logged, mitigating the risk of log injection vulnerabilities.
2023-09-11 08:49:08 +07:00
Yifei Zhang
9770b65146 Merge pull request #2798 from Yidadaa/backup 2023-09-11 00:41:25 +08:00
Yidadaa
57158890c3 fixup 2023-09-11 00:39:56 +08:00
Yifei Zhang
6f8f490fdd Merge pull request #2797 from Yidadaa/backup 2023-09-11 00:36:24 +08:00
Yidadaa
415e9dc913 fixup: minor sync fixup 2023-09-11 00:34:51 +08:00
Yifei Zhang
1487762925 Merge pull request #2796 from Yidadaa/backup 2023-09-11 00:27:51 +08:00
Yidadaa
c73a91a0f5 fixup: fix type errors 2023-09-11 00:24:05 +08:00
Yidadaa
5dced28088 fixup: add en locales 2023-09-11 00:22:14 +08:00
Yidadaa
38f6956e71 feat: close #2754 add import/export to file 2023-09-11 00:20:23 +08:00
Yifei Zhang
93da9dd1f4 Merge pull request #2791 from shoito/patch-1 2023-09-10 21:47:17 +08:00
shoito
505c8cde81 improve japanese translations 2023-09-09 16:10:24 +09:00
Yifei Zhang
f7a6fa9873 Update README_CN.md 2023-09-07 17:43:17 +08:00
Yifei Zhang
70a2d11b8c Merge pull request #2781 from rickyrobinett/main 2023-09-07 10:40:26 +08:00
Ricky Robinett
2c077aca5a fix cloudflare deployment instructions 2023-09-06 15:36:12 -04:00
Yifei Zhang
efef6b098d Merge pull request #2764 from Yidadaa/bugfix-0905 2023-09-05 02:10:53 +08:00
Yidadaa
885f2a3226 feat: close #2752 auto re-fill unfinished input 2023-09-05 01:54:28 +08:00
Yifei Zhang
1f94ae7888 Merge pull request #2721 from Yidadaa/revert-2673-dependabot/npm_and_yarn/next-13.4.19 2023-08-28 11:10:34 +08:00
Yifei Zhang
05aa520669 Revert "chore(deps): bump next from 13.4.9 to 13.4.19" 2023-08-28 11:06:12 +08:00
48 changed files with 2160 additions and 1016 deletions

View File

@@ -15,7 +15,6 @@ BASE_URL=
# Specify OpenAI organization ID.(optional)
# Default: Empty
# If you do not want users to input their own API key, set this value to 1.
OPENAI_ORG_ID=
# (optional)
@@ -31,4 +30,4 @@ DISABLE_GPT4=
# (optional)
# Default: Empty
# If you do not want users to query balance, set this value to 1.
HIDE_BALANCE_QUERY=
HIDE_BALANCE_QUERY=

View File

@@ -114,7 +114,7 @@ OpenAI 接口代理 URL如果你手动配置了 openai 接口代理,请填
OPENAI_API_KEY=<your api key here>
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
BASE_URL=https://chatgpt1.nextweb.fun/api/proxy
BASE_URL=https://nb.nextweb.fun/api/proxy
```
### 本地开发

View File

@@ -4,7 +4,7 @@ import { getServerSideConfig } from "../../config/server";
const serverConfig = getServerSideConfig();
// Danger! Don not write any secret value here!
// Danger! Do not hard code any secret value here!
// 警告!不要在这里写入任何敏感信息!
const DANGER_CONFIG = {
needCode: serverConfig.needCode,

View File

@@ -0,0 +1,43 @@
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",
};
const fetchResult = await fetch(targetUrl, fetchOptions);
console.log("[Any Proxy]", targetUrl, {
status: fetchResult.status,
statusText: fetchResult.statusText,
});
return fetchResult;
}
export const POST = handle;
export const GET = handle;
export const OPTIONS = handle;
export const runtime = "nodejs";

View File

@@ -15,6 +15,8 @@ export function AuthPage() {
const access = useAccessStore();
const goHome = () => navigate(Path.Home);
const goChat = () => navigate(Path.Chat);
const resetAccessCode = () => { access.updateCode(""); access.updateToken(""); }; // Reset access code to empty string
useEffect(() => {
if (getClientConfig()?.isApp) {
@@ -41,14 +43,34 @@ export function AuthPage() {
access.updateCode(e.currentTarget.value);
}}
/>
{!access.hideUserApiKey ? (
<>
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
<input
className={styles["auth-input"]}
type="password"
placeholder={Locale.Settings.Token.Placeholder}
value={access.token}
onChange={(e) => {
access.updateToken(e.currentTarget.value);
}}
/>
</>
) : null}
<div className={styles["auth-actions"]}>
<IconButton
text={Locale.Auth.Confirm}
type="primary"
onClick={goHome}
onClick={goChat}
/>
<IconButton
text={Locale.Auth.Later}
onClick={() => {
resetAccessCode();
goHome();
}}
/>
<IconButton text={Locale.Auth.Later} onClick={goHome} />
</div>
</div>
);

View File

@@ -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 ? 4 : 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>
@@ -1136,7 +1155,13 @@ function _Chat() {
{isUser ? (
<Avatar avatar={config.avatar} />
) : (
<MaskAvatar mask={session.mask} />
<>
{["system"].includes(message.role) ? (
<Avatar avatar="2699-fe0f" />
) : (
<MaskAvatar mask={session.mask} />
)}
</>
)}
</div>

View File

@@ -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();

View File

@@ -433,25 +433,55 @@ export function ImagePreviewer(props: {
const isMobile = useMobileScreen();
const download = () => {
const download = async () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
toPng(dom)
.then((blob) => {
if (!blob) return;
if (isMobile || getClientConfig()?.isApp) {
showImageModal(blob);
const isApp = getClientConfig()?.isApp;
try {
const blob = await toPng(dom);
if (!blob) return;
if (isMobile || (isApp && window.__TAURI__)) {
if (isApp && window.__TAURI__) {
const result = await window.__TAURI__.dialog.save({
defaultPath: `${props.topic}.png`,
filters: [
{
name: "PNG Files",
extensions: ["png"],
},
{
name: "All Files",
extensions: ["*"],
},
],
});
if (result !== null) {
const response = await fetch(blob);
const buffer = await response.arrayBuffer();
const uint8Array = new Uint8Array(buffer);
await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
showToast(Locale.Download.Success);
} else {
showToast(Locale.Download.Failed);
}
} else {
const link = document.createElement("a");
link.download = `${props.topic}.png`;
link.href = blob;
link.click();
refreshPreview();
showImageModal(blob);
}
})
.catch((e) => console.log("[Export Image] ", e));
} else {
const link = document.createElement("a");
link.download = `${props.topic}.png`;
link.href = blob;
link.click();
refreshPreview();
}
} catch (error) {
showToast(Locale.Download.Failed);
}
};
const refreshPreview = () => {

View File

@@ -6,7 +6,7 @@
color: var(--black);
background-color: var(--white);
min-width: 600px;
min-height: 480px;
min-height: 370px;
max-width: 1200px;
display: flex;

View File

@@ -115,7 +115,10 @@ const loadAsyncGoogleFont = () => {
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
linkEl.rel = "stylesheet";
linkEl.href =
googleFontUrl + "/css2?family=Noto+Sans:wght@300;400;700;900&display=swap";
googleFontUrl +
"/css2?family=" +
encodeURIComponent("Noto Sans:wght@300;400;700;900") +
"&display=swap";
document.head.appendChild(linkEl);
};
@@ -125,6 +128,8 @@ function Screen() {
const isHome = location.pathname === Path.Home;
const isAuth = location.pathname === Path.Auth;
const isMobileScreen = useMobileScreen();
const shouldTightBorder =
config.tightBorder && !isMobileScreen && !getClientConfig()?.isApp;
useEffect(() => {
loadAsyncGoogleFont();
@@ -134,11 +139,9 @@ function Screen() {
<div
className={
styles.container +
` ${
config.tightBorder && !isMobileScreen
? styles["tight-container"]
: styles.container
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
getLang() === "ar" ? styles["rtl-screen"] : ""
}`
}
>
{isAuth ? (

View File

@@ -151,6 +151,7 @@ export function Markdown(
ref={mdRef}
onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture}
dir="auto"
>
{props.loading ? (
<LoadingIcon />

View File

@@ -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}
/>

View File

@@ -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";
@@ -40,7 +50,7 @@ import Locale, {
} from "../locales";
import { copyToClipboard } from "../utils";
import Link from "next/link";
import { Path, RELEASE_URL, UPDATE_URL } from "../constant";
import { Path, RELEASE_URL, STORAGE_KEY, UPDATE_URL } from "../constant";
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
import { ErrorBoundary } from "./error";
import { InputRange } from "./input-range";
@@ -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,297 @@ 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={Locale.Settings.Sync.Config.Modal.Check}
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.Settings.Sync.Config.UpStash.Endpoint}>
<input
type="text"
value={syncStore.upstash.endpoint}
onChange={(e) => {
syncStore.update(
(config) =>
(config.upstash.endpoint = e.currentTarget.value),
);
}}
></input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.UpStash.UserName}>
<input
type="text"
value={syncStore.upstash.username}
placeholder={STORAGE_KEY}
onChange={(e) => {
syncStore.update(
(config) =>
(config.upstash.username = e.currentTarget.value),
);
}}
></input>
</ListItem>
<ListItem title={Locale.Settings.Sync.Config.UpStash.Password}>
<PasswordInput
value={syncStore.upstash.apiKey}
onChange={(e) => {
syncStore.update(
(config) => (config.upstash.apiKey = e.currentTarget.value),
);
}}
></PasswordInput>
</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 +797,8 @@ export function Settings() {
</ListItem>
</List>
<SyncItems />
<List>
<ListItem
title={Locale.Settings.Mask.Splash.Title}
@@ -722,8 +959,6 @@ export function Settings() {
</ListItem>
</List>
<SyncItems />
<List>
<ModelConfigList
modelConfig={config.modelConfig}

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useCallback } from "react";
import styles from "./home.module.scss";
@@ -17,6 +17,7 @@ import Locale from "../locales";
import { useAppConfig, useChatStore } from "../store";
import {
DEFAULT_SIDEBAR_WIDTH,
MAX_SIDEBAR_WIDTH,
MIN_SIDEBAR_WIDTH,
NARROW_SIDEBAR_WIDTH,
@@ -57,31 +58,57 @@ function useDragSideBar() {
const config = useAppConfig();
const startX = useRef(0);
const startDragWidth = useRef(config.sidebarWidth ?? 300);
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
const lastUpdateTime = useRef(Date.now());
const handleMouseMove = useRef((e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 50) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
config.update((config) => (config.sidebarWidth = nextWidth));
});
const handleMouseUp = useRef(() => {
startDragWidth.current = config.sidebarWidth ?? 300;
window.removeEventListener("mousemove", handleMouseMove.current);
window.removeEventListener("mouseup", handleMouseUp.current);
});
const onDragMouseDown = (e: MouseEvent) => {
startX.current = e.clientX;
window.addEventListener("mousemove", handleMouseMove.current);
window.addEventListener("mouseup", handleMouseUp.current);
const toggleSideBar = () => {
config.update((config) => {
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
} else {
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
}
});
};
const onDragStart = (e: MouseEvent) => {
// Remembers the initial width each time the mouse is pressed
startX.current = e.clientX;
startDragWidth.current = config.sidebarWidth;
const dragStartTime = Date.now();
const handleDragMove = (e: MouseEvent) => {
if (Date.now() < lastUpdateTime.current + 20) {
return;
}
lastUpdateTime.current = Date.now();
const d = e.clientX - startX.current;
const nextWidth = limit(startDragWidth.current + d);
config.update((config) => {
if (nextWidth < MIN_SIDEBAR_WIDTH) {
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
} else {
config.sidebarWidth = nextWidth;
}
});
};
const handleDragEnd = () => {
// In useRef the data is non-responsive, so `config.sidebarWidth` can't get the dynamic sidebarWidth
window.removeEventListener("pointermove", handleDragMove);
window.removeEventListener("pointerup", handleDragEnd);
// if user click the drag icon, should toggle the sidebar
const shouldFireClick = Date.now() - dragStartTime < 300;
if (shouldFireClick) {
toggleSideBar();
}
};
window.addEventListener("pointermove", handleDragMove);
window.addEventListener("pointerup", handleDragEnd);
};
const isMobileScreen = useMobileScreen();
const shouldNarrow =
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
@@ -89,13 +116,13 @@ function useDragSideBar() {
useEffect(() => {
const barWidth = shouldNarrow
? NARROW_SIDEBAR_WIDTH
: limit(config.sidebarWidth ?? 300);
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
return {
onDragMouseDown,
onDragStart,
shouldNarrow,
};
}
@@ -104,7 +131,7 @@ export function SideBar(props: { className?: string }) {
const chatStore = useChatStore();
// drag side bar
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
const { onDragStart, shouldNarrow } = useDragSideBar();
const navigate = useNavigate();
const config = useAppConfig();
@@ -133,7 +160,13 @@ export function SideBar(props: { className?: string }) {
icon={<MaskIcon />}
text={shouldNarrow ? undefined : Locale.Mask.Name}
className={styles["sidebar-bar-button"]}
onClick={() => navigate(Path.NewChat, { state: { fromHome: true } })}
onClick={() => {
if (config.dontShowMaskSplashScreen !== true) {
navigate(Path.NewChat, { state: { fromHome: true } });
} else {
navigate(Path.Masks, { state: { fromHome: true } });
}
}}
shadow
/>
<IconButton
@@ -198,7 +231,7 @@ export function SideBar(props: { className?: string }) {
<div
className={styles["sidebar-drag"]}
onMouseDown={(e) => onDragMouseDown(e as any)}
onPointerDown={(e) => onDragStart(e as any)}
>
<DragIcon />
</div>

View File

@@ -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_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
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://nb.nextweb.fun";
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
export enum Path {
Home = "/",
@@ -18,6 +20,10 @@ export enum Path {
Auth = "/auth",
}
export enum ApiPath {
Cors = "/api/cors",
}
export enum SlotID {
AppBody = "app-body",
}
@@ -37,6 +43,7 @@ export enum StoreKey {
Sync = "sync",
}
export const DEFAULT_SIDEBAR_WIDTH = 300;
export const MAX_SIDEBAR_WIDTH = 500;
export const MIN_SIDEBAR_WIDTH = 230;
export const NARROW_SIDEBAR_WIDTH = 100;
@@ -44,6 +51,9 @@ export const NARROW_SIDEBAR_WIDTH = 100;
export const ACCESS_CODE_PREFIX = "nk-";
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;

12
app/global.d.ts vendored
View File

@@ -13,5 +13,17 @@ declare module "*.svg";
declare interface Window {
__TAURI__?: {
writeText(text: string): Promise<void>;
invoke(command: string, payload?: Record<string, unknown>): Promise<any>;
dialog: {
save(options?: Record<string, unknown>): Promise<string | null>;
};
fs: {
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
};
notification:{
requestPermission(): Promise<Permission>;
isPermissionGranted(): Promise<boolean>;
sendNotification(options: string | Options): void;
};
};
}

1
app/icons/cloud-fail.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -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

1
app/icons/config.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

1
app/icons/connection.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -10,6 +10,7 @@ const ar: PartialLocaleType = {
Auth: {
Title: "تحتاج إلى رمز الوصول",
Tips: "يرجى إدخال رمز الوصول أدناه",
SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك",
Input: "رمز الوصول",
Confirm: "تأكيد",
Later: "لاحقًا",

View File

@@ -10,6 +10,7 @@ const bn: PartialLocaleType = {
Auth: {
Title: "একটি অ্যাক্সেস কোড প্রয়োজন",
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন",
SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করুন",
Input: "অ্যাক্সেস কোড",
Confirm: "নিশ্চিত করুন",
Later: "পরে",

View File

@@ -13,6 +13,7 @@ const cn = {
Auth: {
Title: "需要密码",
Tips: "管理员开启了密码验证,请在下方填入访问码",
SubTips: "或者输入你的 OpenAI API 密钥",
Input: "在此处填写访问码",
Confirm: "确认",
Later: "稍后再说",
@@ -178,6 +179,49 @@ const cn = {
Title: "自动生成标题",
SubTitle: "根据对话内容生成合适的标题",
},
Sync: {
CloudState: "云端数据",
NotSyncYet: "还没有进行过同步",
Success: "同步成功",
Fail: "同步失败",
Config: {
Modal: {
Title: "配置云同步",
Check: "检查可用性",
},
SyncType: {
Title: "同步类型",
SubTitle: "选择喜爱的同步服务器",
},
Proxy: {
Title: "启用代理",
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
},
ProxyUrl: {
Title: "代理地址",
SubTitle: "仅适用于本项目自带的跨域代理",
},
WebDav: {
Endpoint: "WebDAV 地址",
UserName: "用户名",
Password: "密码",
},
UpStash: {
Endpoint: "UpStash Redis REST Url",
UserName: "备份名称",
Password: "UpStash Redis REST Token",
},
},
LocalState: "本地数据",
Overview: (overview: any) => {
return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`;
},
ImportFailed: "导入失败",
},
Mask: {
Splash: {
Title: "面具启动页",
@@ -280,6 +324,10 @@ const cn = {
Success: "已写入剪切板",
Failed: "复制失败,请赋予剪切板权限",
},
Download: {
Success: "内容已下载到您的目录。",
Failed: "下载失败。",
},
Context: {
Toast: (x: any) => `包含 ${x} 条预设提示词`,
Edit: "当前对话设置",
@@ -355,6 +403,10 @@ const cn = {
Close: "关闭",
Create: "新建",
Edit: "编辑",
Export: "导出",
Import: "导入",
Sync: "同步",
Config: "配置",
},
Exporter: {
Model: "模型",

View File

@@ -15,6 +15,7 @@ const en: LocaleType = {
Auth: {
Title: "Need Access Code",
Tips: "Please enter access code below",
SubTips: "Or enter your OpenAI API Key",
Input: "access code",
Confirm: "Confirm",
Later: "Later",
@@ -180,6 +181,50 @@ const en: LocaleType = {
Title: "Auto Generate Title",
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",
Check: "Check Connection",
},
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",
},
UpStash: {
Endpoint: "UpStash Redis REST Url",
UserName: "Backup Name",
Password: "UpStash Redis REST Token",
},
},
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: {
Splash: {
Title: "Mask Splash Screen",
@@ -285,6 +330,10 @@ const en: LocaleType = {
Success: "Copied to clipboard",
Failed: "Copy failed, please grant permission to access clipboard",
},
Download: {
Success: "Content downloaded to your directory.",
Failed: "Download failed.",
},
Context: {
Toast: (x: any) => `With ${x} contextual prompts`,
Edit: "Current Chat Settings",
@@ -355,6 +404,10 @@ const en: LocaleType = {
Close: "Close",
Create: "Create",
Edit: "Edit",
Export: "Export",
Import: "Import",
Sync: "Sync",
Config: "Config",
},
Exporter: {
Model: "Model",

View File

@@ -4,12 +4,12 @@ import { PartialLocaleType } from "./index";
const id: PartialLocaleType = {
WIP: "Coming Soon...",
Error: {
Unauthorized:
"Akses tidak diizinkan. Silakan [otorisasi](/#/auth) dengan memasukkan kode akses.",
},
Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
},
Auth: {
Title: "Diperlukan Kode Akses",
Tips: "Masukkan kode akses di bawah",
SubTips: "Atau masukkan kunci API OpenAI Anda",
Input: "Kode Akses",
Confirm: "Konfirmasi",
Later: "Nanti",
@@ -60,7 +60,9 @@ const id: PartialLocaleType = {
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", Shift + Enter untuk membalut";
}
return inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah";
return (
inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah"
);
},
Send: "Kirim",
Config: {
@@ -117,33 +119,35 @@ const id: PartialLocaleType = {
Title: "Setel Ulang Semua Pengaturan",
SubTitle: "Mengembalikan semua pengaturan ke nilai default",
Action: "Setel Ulang",
Confirm: "Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
Confirm:
"Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
},
Clear: {
Title: "Hapus Semua Data",
SubTitle: "Menghapus semua pesan dan pengaturan",
SubTitle: "Semua data yang tersimpan secara lokal akan dihapus",
Action: "Hapus",
Confirm: "Anda yakin ingin menghapus semua pesan dan pengaturan?",
Confirm:
"Apakah Anda yakin ingin menghapus semua data yang tersimpan secara lokal?",
},
},
Lang: {
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Semua Bahasa",
},
Avatar: "Avatar",
FontSize: {
Title: "Ukuran Font",
SubTitle: "Ubah ukuran font konten chat",
},
InjectSystemPrompts: {
Title: "Suntikkan Petunjuk Sistem",
SubTitle:
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
},
InputTemplate: {
Title: "Template Input",
SubTitle: "Pesan baru akan diisi menggunakan template ini",
},
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
All: "Semua Bahasa",
},
Avatar: "Avatar",
FontSize: {
Title: "Ukuran Font",
SubTitle: "Ubah ukuran font konten chat",
},
InjectSystemPrompts: {
Title: "Suntikkan Petunjuk Sistem",
SubTitle:
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
},
InputTemplate: {
Title: "Template Input",
SubTitle: "Pesan baru akan diisi menggunakan template ini",
},
Update: {
Version: (x: string) => `Version: ${x}`,
@@ -154,9 +158,40 @@ const id: PartialLocaleType = {
GoToUpdate: "Perbarui Sekarang",
},
AutoGenerateTitle: {
Title: "Hasilkan Judul Otomatis",
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
Title: "Hasilkan Judul Otomatis",
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
},
Sync: {
CloudState: "Pembaruan Terakhir",
NotSyncYet: "Belum disinkronkan",
Success: "Sinkronisasi Berhasil",
Fail: "Sinkronisasi Gagal",
Config: {
Modal: {
Title: "Konfigurasi Sinkronisasi",
},
SyncType: {
Title: "Tipe Sinkronisasi",
SubTitle: "Pilih layanan sinkronisasi favorit Anda",
},
Proxy: {
Title: "Aktifkan Proxy CORS",
SubTitle:
"Aktifkan Proxy untuk menghindari pembatasan atau pemblokiran lintas sumber",
},
ProxyUrl: {
Title: "Lokasi Titik Akhir Proxy CORS",
SubTitle: "Hanya berlaku untuk Proxy CORS bawaan untuk proyek ini",
},
WebDav: {
Endpoint: "Lokasi Titik Akhir WebDAV",
UserName: "User Pengguna",
Password: "Kata Sandi",
},
},
},
SendKey: "Kirim",
Theme: "Tema",
TightBorder: "Batas Ketat",
@@ -176,76 +211,77 @@ const id: PartialLocaleType = {
},
},
Prompt: {
Disable: {
Title: "Nonaktifkan Otomatisasi",
SubTitle: "Aktifkan/Matikan otomatisasi",
},
List: "Daftar Prompt",
ListCount: (builtin: number, custom: number) =>
`${builtin} bawaan, ${custom} penggunaan khusus`,
Edit: "Edit",
Modal: {
Title: "Daftar Prompt",
Add: "Tambahkan",
Search: "Cari Prompt",
},
EditModal: {
Title: "Edit Prompt",
},
Disable: {
Title: "Nonaktifkan Otomatisasi",
SubTitle: "Aktifkan/Matikan otomatisasi",
},
HistoryCount: {
Title: "Jumlah Pesan Riwayat",
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
List: "Daftar Prompt",
ListCount: (builtin: number, custom: number) =>
`${builtin} bawaan, ${custom} penggunaan khusus`,
Edit: "Edit",
Modal: {
Title: "Daftar Prompt",
Add: "Tambahkan",
Search: "Cari Prompt",
},
CompressThreshold: {
Title: "Batas Kompresi Riwayat",
SubTitle:
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
},
Token: {
Title: "Kunci API",
SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses",
Placeholder: "Kunci API OpenAI",
EditModal: {
Title: "Edit Prompt",
},
Usage: {
Title: "Saldo Akun",
SubTitle(used: any, total: any) {
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
},
IsChecking: "Memeriksa...",
Check: "Periksa",
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
},
HistoryCount: {
Title: "Jumlah Pesan Riwayat",
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
},
CompressThreshold: {
Title: "Batas Kompresi Riwayat",
SubTitle:
"Jika panjang pesan melebihi batas yang ditentukan, pesan tersebut akan dikompresi",
},
Token: {
Title: "Kunci API",
SubTitle: "Gunakan kunci Anda untuk melewati batas kode akses",
Placeholder: "Kunci API OpenAI",
},
Usage: {
Title: "Saldo Akun",
SubTitle(used: any, total: any) {
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
},
AccessCode: {
Title: "Kode Akses",
SubTitle: "Kontrol akses diaktifkan",
Placeholder: "Diperlukan kode akses",
},
Endpoint: {
Title: "Endpoint",
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
},
Model: "Model",
Temperature: {
Title: "Suhu",
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
},
TopP: {
Title: "Top P",
SubTitle: "Tidak mengubah nilai dengan suhu",
},
MaxTokens: {
Title: "Token Maksimum",
SubTitle: "Panjang maksimum token input dan output",
},
PresencePenalty: {
Title: "Penalti Kehadiran",
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
},
FrequencyPenalty: {
Title: "Penalti Frekuensi",
SubTitle: "Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
},
IsChecking: "Memeriksa...",
Check: "Periksa",
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
},
AccessCode: {
Title: "Kode Akses",
SubTitle: "Kontrol akses diaktifkan",
Placeholder: "Diperlukan kode akses",
},
Endpoint: {
Title: "Endpoint",
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
},
Model: "Model",
Temperature: {
Title: "Suhu",
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
},
TopP: {
Title: "Top P",
SubTitle: "Tidak mengubah nilai dengan suhu",
},
MaxTokens: {
Title: "Token Maksimum",
SubTitle: "Panjang maksimum token input dan output",
},
PresencePenalty: {
Title: "Penalti Kehadiran",
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
},
FrequencyPenalty: {
Title: "Penalti Frekuensi",
SubTitle:
"Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
},
},
Store: {
DefaultTopic: "Percakapan Baru",
@@ -261,8 +297,13 @@ const id: PartialLocaleType = {
},
},
Copy: {
Success: "Berhasil disalin ke clipboard",
Failed: "Gagal menyalin, berikan izin untuk memberikan izin",
Success: "Tersalin ke clipboard",
Failed:
"Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)",
},
Download: {
Success: "Konten berhasil diunduh ke direktori Anda.",
Failed: "Unduhan gagal.",
},
Context: {
Toast: (x: any) => `Dengan ${x} promp kontekstual`,
@@ -341,7 +382,7 @@ const id: PartialLocaleType = {
Model: "Model",
Messages: "Pesan",
Topic: "Topik",
Time: "Waktu",
Time: "Tanggal & Waktu",
},
URLCommand: {
Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ",

View File

@@ -19,7 +19,11 @@ const jp: PartialLocaleType = {
Copy: "コピー",
Stop: "停止",
Retry: "リトライ",
Pin: "ピン",
PinToastContent: "コンテキストプロンプトに1つのメッセージをピン留めしました",
PinToastAction: "表示",
Delete: "削除",
Edit: "編集",
},
Rename: "チャットの名前を変更",
Typing: "入力中…",
@@ -33,7 +37,7 @@ const jp: PartialLocaleType = {
Send: "送信",
Config: {
Reset: "リセット",
SaveAs: "另存为面具",
SaveAs: "保存",
},
},
Export: {

View File

@@ -1,28 +1,7 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { DEFAULT_API_HOST, DEFAULT_MODELS, StoreKey } from "../constant";
import { getHeaders } from "../client/api";
import { BOT_HELLO } from "./chat";
import { getClientConfig } from "../config/client";
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;
}
import { createPersistStore } from "../utils/store";
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/";
console.log("[API] default openai url", DEFAULT_OPENAI_URL);
export const useAccessStore = create<AccessControlStore>()(
persist(
(set, get) => ({
token: "",
accessCode: "",
needCode: true,
hideUserApiKey: false,
hideBalanceQuery: false,
disableGPT4: false,
const DEFAULT_ACCESS_STATE = {
token: "",
accessCode: "",
needCode: true,
hideUserApiKey: false,
hideBalanceQuery: false,
disableGPT4: false,
openaiUrl: DEFAULT_OPENAI_URL,
openaiUrl: DEFAULT_OPENAI_URL,
};
enabledAccessControl() {
get().fetch();
export const useAccessStore = createPersistStore(
{ ...DEFAULT_ACCESS_STATE },
return get().needCode;
},
updateCode(code: string) {
set(() => ({ accessCode: code?.trim() }));
},
updateToken(token: string) {
set(() => ({ token: token?.trim() }));
},
updateOpenAiUrl(url: string) {
set(() => ({ openaiUrl: url?.trim() }));
},
isAuthorized() {
get().fetch();
(set, get) => ({
enabledAccessControl() {
this.fetch();
// has token or has code or disabled access control
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,
return get().needCode;
},
),
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,
},
);

View File

@@ -18,6 +18,7 @@ import { ChatControllerPool } from "../client/controller";
import { prettyObject } from "../utils/format";
import { estimateTokenLength } from "../utils/token";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
export type ChatMessage = RequestMessage & {
date: string;
@@ -140,12 +141,22 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
return output;
}
export const useChatStore = create<ChatStore>()(
persist(
(set, get) => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
const DEFAULT_CHAT_STATE = {
sessions: [createEmptySession()],
currentSessionIndex: 0,
};
export const useChatStore = createPersistStore(
DEFAULT_CHAT_STATE,
(set, _get) => {
function get() {
return {
..._get(),
...methods,
};
}
const methods = {
clearSessions() {
set(() => ({
sessions: [createEmptySession()],
@@ -184,7 +195,7 @@ export const useChatStore = create<ChatStore>()(
});
},
newSession(mask) {
newSession(mask?: Mask) {
const session = createEmptySession();
if (mask) {
@@ -207,14 +218,14 @@ export const useChatStore = create<ChatStore>()(
}));
},
nextSession(delta) {
nextSession(delta: number) {
const n = get().sessions.length;
const limit = (x: number) => (x + n) % n;
const i = get().currentSessionIndex;
get().selectSession(limit(i + delta));
},
deleteSession(index) {
deleteSession(index: number) {
const deletingLastSession = get().sessions.length === 1;
const deletedSession = get().sessions.at(index);
@@ -271,7 +282,7 @@ export const useChatStore = create<ChatStore>()(
return session;
},
onNewMessage(message) {
onNewMessage(message: ChatMessage) {
get().updateCurrentSession((session) => {
session.messages = session.messages.concat();
session.lastUpdate = Date.now();
@@ -280,7 +291,7 @@ export const useChatStore = create<ChatStore>()(
get().summarizeSession();
},
async onUserInput(content) {
async onUserInput(content: string) {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
@@ -580,14 +591,14 @@ export const useChatStore = create<ChatStore>()(
}
},
updateStat(message) {
updateStat(message: ChatMessage) {
get().updateCurrentSession((session) => {
session.stat.charCount += message.content.length;
// TODO: should update chat count and word count
});
},
updateCurrentSession(updater) {
updateCurrentSession(updater: (session: ChatSession) => void) {
const sessions = get().sessions;
const index = get().currentSessionIndex;
updater(sessions[index]);
@@ -598,56 +609,60 @@ export const useChatStore = create<ChatStore>()(
localStorage.clear();
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) {
newState.sessions = [];
return methods;
},
{
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;
for (const oldSession of oldSessions) {
const newSession = createEmptySession();
newSession.topic = oldSession.topic;
newSession.messages = [...oldSession.messages];
newSession.mask.modelConfig.sendMemory = true;
newSession.mask.modelConfig.historyMessageCount = 4;
newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
newState.sessions.push(newSession);
if (version < 2) {
newState.sessions = [];
const oldSessions = state.sessions;
for (const oldSession of oldSessions) {
const newSession = createEmptySession();
newSession.topic = oldSession.topic;
newSession.messages = [...oldSession.messages];
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) {
// 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;
},
return newState as any;
},
),
},
);

View File

@@ -1,8 +1,13 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { LLMModel } from "../client/api";
import { isMacOS } from "../utils";
import { getClientConfig } from "../config/client";
import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant";
import {
DEFAULT_INPUT_TEMPLATE,
DEFAULT_MODELS,
DEFAULT_SIDEBAR_WIDTH,
StoreKey,
} from "../constant";
import { createPersistStore } from "../utils/store";
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
@@ -21,14 +26,16 @@ export enum Theme {
}
export const DEFAULT_CONFIG = {
submitKey: SubmitKey.CtrlEnter as SubmitKey,
lastUpdate: Date.now(), // timestamp, to merge state
submitKey: isMacOS() ? SubmitKey.MetaEnter : SubmitKey.CtrlEnter,
avatar: "1f603",
fontSize: 14,
theme: Theme.Auto as Theme,
tightBorder: !!getClientConfig()?.isApp,
sendPreviewBubble: true,
enableAutoGenerateTitle: true,
sidebarWidth: 300,
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
disablePromptHint: false,
@@ -55,13 +62,6 @@ export const 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 function limitNumber(
@@ -98,85 +98,80 @@ export const ModalConfigValidator = {
},
};
export const useAppConfig = create<ChatConfigStore>()(
persist(
(set, get) => ({
...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;
},
export const useAppConfig = createPersistStore(
{ ...DEFAULT_CONFIG },
(set, get) => ({
reset() {
set(() => ({ ...DEFAULT_CONFIG }));
},
),
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;
},
},
);

View File

@@ -1,11 +1,10 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
import { ModelConfig, useAppConfig } from "./config";
import { StoreKey } from "../constant";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
export type Mask = {
id: string;
@@ -25,14 +24,6 @@ export const 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 createEmptyMask = () =>
@@ -46,89 +37,90 @@ export const createEmptyMask = () =>
lang: getLang(),
builtin: false,
createdAt: Date.now(),
} as Mask);
}) as Mask;
export const useMaskStore = create<MaskStore>()(
persist(
(set, get) => ({
...DEFAULT_MASK_STATE,
export const useMaskStore = createPersistStore(
{ ...DEFAULT_MASK_STATE },
create(mask) {
const masks = get().masks;
const id = nanoid();
masks[id] = {
...createEmptyMask(),
...mask,
id,
builtin: false,
};
(set, get) => ({
create(mask?: Partial<Mask>) {
const masks = get().masks;
const id = nanoid();
masks[id] = {
...createEmptyMask(),
...mask,
id,
builtin: false,
};
set(() => ({ masks }));
set(() => ({ masks }));
get().markUpdate();
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;
},
return masks[id];
},
),
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;
},
},
);

View File

@@ -1,9 +1,8 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import Fuse from "fuse.js";
import { getLang } from "../locales";
import { StoreKey } from "../constant";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
export interface Prompt {
id: string;
@@ -13,19 +12,6 @@ export interface Prompt {
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 = {
ready: false,
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
@@ -62,130 +48,136 @@ export const SearchService = {
},
};
export const usePromptStore = create<PromptStore>()(
persist(
(set, get) => ({
counter: 0,
latestId: 0,
prompts: {},
export const usePromptStore = createPersistStore(
{
counter: 0,
prompts: {} as Record<string, Prompt>,
},
add(prompt) {
const prompts = get().prompts;
prompt.id = nanoid();
prompt.isUser = true;
prompt.createdAt = Date.now();
prompts[prompt.id] = prompt;
(set, get) => ({
add(prompt: Prompt) {
const prompts = get().prompts;
prompt.id = nanoid();
prompt.isUser = true;
prompt.createdAt = Date.now();
prompts[prompt.id] = prompt;
set(() => ({
latestId: prompt.id!,
prompts: prompts,
}));
set(() => ({
prompts: prompts,
}));
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);
});
},
return prompt.id!;
},
),
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);
});
},
},
);

View File

@@ -1,7 +1,19 @@
import { getClientConfig } from "../config/client";
import { Updater } from "../typing";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { StoreKey } from "../constant";
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
import { createPersistStore } from "../utils/store";
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 {
server: string;
@@ -9,79 +21,111 @@ export interface WebDavConfig {
password: string;
}
export interface SyncStore {
webDavConfig: WebDavConfig;
lastSyncTime: number;
const isApp = !!getClientConfig()?.isApp;
export type SyncStore = GetStoreState<typeof useSyncStore>;
update: Updater<WebDavConfig>;
check: () => Promise<boolean>;
const DEFAULT_SYNC_STATE = {
provider: ProviderType.WebDAV,
useProxy: true,
proxyUrl: corsPath(ApiPath.Cors),
path: (path: string) => string;
headers: () => { Authorization: string };
}
webdav: {
endpoint: "",
username: "",
password: "",
},
const FILE = {
root: "/chatgpt-next-web/",
upstash: {
endpoint: "",
username: STORAGE_KEY,
apiKey: "",
},
lastSyncTime: 0,
lastProvider: "",
};
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,
export const useSyncStore = createPersistStore(
DEFAULT_SYNC_STATE,
(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 datePart = isApp
? `${new Date().toLocaleDateString().replace(/\//g, '_')} ${new Date().toLocaleTimeString().replace(/:/g, '_')}`
: new Date().toLocaleString();
const fileName = `Backup-${datePart}.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,
migrate(persistedState, version) {
const newState = persistedState as typeof DEFAULT_SYNC_STATE;
if (version < 1.1) {
newState.upstash.username = STORAGE_KEY;
}
return newState as any;
},
},
);

View File

@@ -1,26 +1,12 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
import { api } from "../client/api";
import { getClientConfig } from "../config/client";
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;
}
import { createPersistStore } from "../utils/store";
import ChatGptIcon from "../icons/chatgpt.png";
import Locale from "../locales";
const ONE_MINUTE = 60 * 1000;
const isApp = !!getClientConfig()?.isApp;
function formatVersionDate(t: string) {
const d = new Date(+t);
@@ -35,7 +21,9 @@ function formatVersionDate(t: string) {
].join("");
}
async function getVersion(type: "date" | "tag") {
type VersionType = "date" | "tag";
async function getVersion(type: VersionType) {
if (type === "date") {
const data = (await (await fetch(FETCH_COMMIT_URL)).json()) as {
commit: {
@@ -55,75 +43,108 @@ async function getVersion(type: "date" | "tag") {
}
}
export const useUpdateStore = create<UpdateStore>()(
persist(
(set, get) => ({
versionType: "tag",
lastUpdate: 0,
version: "unknown",
remoteVersion: "",
export const useUpdateStore = createPersistStore(
{
versionType: "tag" as VersionType,
lastUpdate: 0,
version: "unknown",
remoteVersion: "",
used: 0,
subscription: 0,
lastUpdateUsage: 0,
formatVersion(version: string) {
if (get().versionType === "date") {
version = formatVersionDate(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,
lastUpdateUsage: 0,
},
(set, get) => ({
formatVersion(version: string) {
if (get().versionType === "date") {
version = formatVersionDate(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,
}));
if (window.__TAURI__?.notification && isApp) {
// Check if notification permission is granted
await window.__TAURI__?.notification.isPermissionGranted().then((granted) => {
if (!granted) {
return;
} else {
// Request permission to show notifications
window.__TAURI__?.notification.requestPermission().then((permission) => {
if (permission === 'granted') {
if (version === remoteId) {
// Show a notification using Tauri
window.__TAURI__?.notification.sendNotification({
title: "ChatGPT Next Web",
body: `${Locale.Settings.Update.IsLatest}`,
icon: `${ChatGptIcon.src}`,
sound: "Default"
});
} else {
const updateMessage = Locale.Settings.Update.FoundUpdate(`${remoteId}`);
// Show a notification for the new version using Tauri
window.__TAURI__?.notification.sendNotification({
title: "ChatGPT Next Web",
body: updateMessage,
icon: `${ChatGptIcon.src}`,
sound: "Default"
});
}
}
});
}
});
}
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,
},
);

View File

@@ -31,12 +31,41 @@ export async function copyToClipboard(text: string) {
}
}
export function downloadAs(text: string, filename: string) {
const element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
);
export async function downloadAs(text: string, filename: string) {
if (window.__TAURI__) {
const result = await window.__TAURI__.dialog.save({
defaultPath: `${filename}`,
filters: [
{
name: `${filename.split('.').pop()} files`,
extensions: [`${filename.split('.').pop()}`],
},
{
name: "All Files",
extensions: ["*"],
},
],
});
if (result !== null) {
try {
await window.__TAURI__.fs.writeBinaryFile(
result,
new Uint8Array([...text].map((c) => c.charCodeAt(0)))
);
showToast(Locale.Download.Success);
} catch (error) {
showToast(Locale.Download.Failed);
}
} else {
showToast(Locale.Download.Failed);
}
} else {
const element = document.createElement("a");
element.setAttribute(
"href",
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
);
element.setAttribute("download", filename);
element.style.display = "none";
@@ -46,7 +75,7 @@ export function downloadAs(text: string, filename: string) {
document.body.removeChild(element);
}
}
export function readFromFile() {
return new Promise<string>((res, rej) => {
const fileInput = document.createElement("input");
@@ -173,3 +202,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
export function getCSSVar(varName: string) {
return getComputedStyle(document.body).getPropertyValue(varName).trim();
}
/**
* Detects Macintosh
*/
export function isMacOS(): boolean {
if (typeof window !== "undefined") {
let userAgent = window.navigator.userAgent.toLocaleLowerCase();
const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent)
return !!macintosh
}
return false
}

3
app/utils/clone.ts Normal file
View File

@@ -0,0 +1,3 @@
export function deepClone<T>(obj: T) {
return JSON.parse(JSON.stringify(obj));
}

33
app/utils/cloud/index.ts Normal file
View File

@@ -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;
}

101
app/utils/cloud/upstash.ts Normal file
View File

@@ -0,0 +1,101 @@
import { STORAGE_KEY } from "@/app/constant";
import { SyncStore } from "@/app/store/sync";
import { corsFetch } from "../cors";
import { chunks } from "../format";
export type UpstashConfig = SyncStore["upstash"];
export type UpStashClient = ReturnType<typeof createUpstashClient>;
export function createUpstashClient(store: SyncStore) {
const config = store.upstash;
const storeKey = config.username.length === 0 ? STORAGE_KEY : config.username;
const chunkCountKey = `${storeKey}-chunk-count`;
const chunkIndexKey = (i: number) => `${storeKey}-chunk-${i}`;
const proxyUrl =
store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined;
return {
async check() {
try {
const res = await corsFetch(this.path(`get/${storeKey}`), {
method: "GET",
headers: this.headers(),
proxyUrl,
});
console.log("[Upstash] check", res.status, res.statusText);
return [200].includes(res.status);
} catch (e) {
console.error("[Upstash] failed to check", e);
}
return false;
},
async redisGet(key: string) {
const res = await corsFetch(this.path(`get/${key}`), {
method: "GET",
headers: this.headers(),
proxyUrl,
});
console.log("[Upstash] get key = ", key, res.status, res.statusText);
const resJson = (await res.json()) as { result: string };
return resJson.result;
},
async redisSet(key: string, value: string) {
const res = await corsFetch(this.path(`set/${key}`), {
method: "POST",
headers: this.headers(),
body: value,
proxyUrl,
});
console.log("[Upstash] set key = ", key, res.status, res.statusText);
},
async get() {
const chunkCount = Number(await this.redisGet(chunkCountKey));
if (!Number.isInteger(chunkCount)) return;
const chunks = await Promise.all(
new Array(chunkCount)
.fill(0)
.map((_, i) => this.redisGet(chunkIndexKey(i))),
);
console.log("[Upstash] get full chunks", chunks);
return chunks.join("");
},
async set(_: string, value: string) {
// upstash limit the max request size which is 1Mb for “Free” and “Pay as you go”
// so we need to split the data to chunks
let index = 0;
for await (const chunk of chunks(value)) {
await this.redisSet(chunkIndexKey(index), chunk);
index += 1;
}
await this.redisSet(chunkCountKey, index.toString());
},
headers() {
return {
Authorization: `Bearer ${config.apiKey}`,
};
},
path(path: string) {
let url = config.endpoint;
if (!url.endsWith("/")) {
url += "/";
}
if (path.startsWith("/")) {
path = path.slice(1);
}
return url + path;
},
};
}

76
app/utils/cloud/webdav.ts Normal file
View File

@@ -0,0 +1,76 @@
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, 301, 302, 307, 308].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;
},
};
}

50
app/utils/cors.ts Normal file
View File

@@ -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);
}

View File

@@ -11,3 +11,18 @@ export function prettyObject(msg: any) {
}
return ["```json", msg, "```"].join("\n");
}
export function* chunks(s: string, maxBytes = 1000 * 1000) {
const decoder = new TextDecoder("utf-8");
let buf = new TextEncoder().encode(s);
while (buf.length) {
let i = buf.lastIndexOf(32, maxBytes + 1);
// If no space found, try forward search
if (i < 0) i = buf.indexOf(32, maxBytes);
// If there's no space at all, take all
if (i < 0) i = buf.length;
// This is a safe cut-off point; never half-way a multi-byte
yield decoder.decode(buf.slice(0, i));
buf = buf.slice(i + 1); // Skip space (if any)
}
}

55
app/utils/store.ts Normal file
View File

@@ -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),
);
}

165
app/utils/sync.ts Normal file
View File

@@ -0,0 +1,165 @@
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) => {
// skip empty chats
if (remoteSession.messages.length === 0) return;
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 };
}
}

View File

@@ -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".
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.
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".
15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag".
16. Go to "Deployments" and click "Retry deployment".
17. Enjoy.
17. Enjoy.

View File

@@ -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") {
nextConfig.headers = async () => {
return [
{
source: "/api/:path*",
headers: [
{ 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",
},
],
headers: CorsHeaders,
},
];
};
@@ -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 {
beforeFiles: ret,
};

View File

@@ -20,17 +20,17 @@
"@hello-pangea/dnd": "^16.3.0",
"@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.4.7",
"emoji-picker-react": "^4.5.1",
"fuse.js": "^6.6.2",
"html-to-image": "^1.11.11",
"mermaid": "^10.3.1",
"nanoid": "^4.0.2",
"next": "^13.4.19",
"next": "^13.4.9",
"node-fetch": "^3.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.14.1",
"react-router-dom": "^6.15.0",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.3",
"remark-breaks": "^3.0.2",
@@ -49,14 +49,14 @@
"@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.2",
"cross-env": "^7.0.3",
"eslint": "^8.44.0",
"eslint": "^8.49.0",
"eslint-config-next": "13.4.19",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.0",
"lint-staged": "^13.2.2",
"prettier": "^3.0.2",
"typescript": "4.9.5",
"typescript": "5.2.2",
"webpack": "^5.88.1"
},
"resolutions": {

View File

@@ -17,7 +17,7 @@ tauri-build = { version = "1.3.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.3.0", features = ["clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri = { version = "1.3.0", features = ["notification-all", "fs-all", "clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
[features]

View File

@@ -9,7 +9,7 @@
},
"package": {
"productName": "ChatGPT Next Web",
"version": "2.9.5"
"version": "2.9.8"
},
"tauri": {
"allowlist": {
@@ -44,6 +44,12 @@
"startDragging": true,
"unmaximize": true,
"unminimize": true
},
"fs": {
"all": true
},
"notification": {
"all": true
}
},
"bundle": {

242
yarn.lock
View File

@@ -1012,15 +1012,15 @@
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.4.0":
version "4.5.0"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.0.tgz#f6f729b02feee2c749f57e334b7a1b5f40a81724"
integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==
"@eslint-community/regexpp@^4.6.1":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.8.0.tgz#11195513186f68d42fbf449f9a7136b2c0c92005"
integrity sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==
"@eslint/eslintrc@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d"
integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==
"@eslint/eslintrc@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396"
integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@@ -1032,10 +1032,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.44.0":
version "8.44.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af"
integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==
"@eslint/js@8.49.0":
version "8.49.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333"
integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==
"@fortaine/fetch-event-source@^3.0.6":
version "3.0.6"
@@ -1055,10 +1055,10 @@
redux "^4.2.1"
use-memo-one "^1.1.3"
"@humanwhocodes/config-array@^0.11.10":
version "0.11.10"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==
"@humanwhocodes/config-array@^0.11.11":
version "0.11.11"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==
dependencies:
"@humanwhocodes/object-schema" "^1.2.1"
debug "^4.1.1"
@@ -1131,10 +1131,10 @@
"@jridgewell/resolve-uri" "3.1.0"
"@jridgewell/sourcemap-codec" "1.4.14"
"@next/env@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.19.tgz#46905b4e6f62da825b040343cbc233144e9578d3"
integrity sha512-FsAT5x0jF2kkhNkKkukhsyYOrRqtSxrEhfliniIq0bwWbuXLgyt3Gv0Ml+b91XwjwArmuP7NxCiGd++GGKdNMQ==
"@next/env@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.4.9.tgz#b77759514dd56bfa9791770755a2482f4d6ca93e"
integrity sha512-vuDRK05BOKfmoBYLNi2cujG2jrYbEod/ubSSyqgmEx9n/W3eZaJQdRNhTfumO+qmq/QTzLurW487n/PM/fHOkw==
"@next/eslint-plugin-next@13.4.19":
version "13.4.19"
@@ -1143,50 +1143,50 @@
dependencies:
glob "7.1.7"
"@next/swc-darwin-arm64@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.19.tgz#77ad462b5ced4efdc26cb5a0053968d2c7dac1b6"
integrity sha512-vv1qrjXeGbuF2mOkhkdxMDtv9np7W4mcBtaDnHU+yJG+bBwa6rYsYSCI/9Xm5+TuF5SbZbrWO6G1NfTh1TMjvQ==
"@next/swc-darwin-arm64@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.9.tgz#0ed408d444bbc6b0a20f3506a9b4222684585677"
integrity sha512-TVzGHpZoVBk3iDsTOQA/R6MGmFp0+17SWXMEWd6zG30AfuELmSSMe2SdPqxwXU0gbpWkJL1KgfLzy5ReN0crqQ==
"@next/swc-darwin-x64@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.19.tgz#aebe38713a4ce536ee5f2a291673e14b715e633a"
integrity sha512-jyzO6wwYhx6F+7gD8ddZfuqO4TtpJdw3wyOduR4fxTUCm3aLw7YmHGYNjS0xRSYGAkLpBkH1E0RcelyId6lNsw==
"@next/swc-darwin-x64@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.9.tgz#a08fccdee68201522fe6618ec81f832084b222f8"
integrity sha512-aSfF1fhv28N2e7vrDZ6zOQ+IIthocfaxuMWGReB5GDriF0caTqtHttAvzOMgJgXQtQx6XhyaJMozLTSEXeNN+A==
"@next/swc-linux-arm64-gnu@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.19.tgz#ec54db65b587939c7b94f9a84800f003a380f5a6"
integrity sha512-vdlnIlaAEh6H+G6HrKZB9c2zJKnpPVKnA6LBwjwT2BTjxI7e0Hx30+FoWCgi50e+YO49p6oPOtesP9mXDRiiUg==
"@next/swc-linux-arm64-gnu@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.9.tgz#1798c2341bb841e96521433eed00892fb24abbd1"
integrity sha512-JhKoX5ECzYoTVyIy/7KykeO4Z2lVKq7HGQqvAH+Ip9UFn1MOJkOnkPRB7v4nmzqAoY+Je05Aj5wNABR1N18DMg==
"@next/swc-linux-arm64-musl@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.19.tgz#1f5e2c1ea6941e7d530d9f185d5d64be04279d86"
integrity sha512-aU0HkH2XPgxqrbNRBFb3si9Ahu/CpaR5RPmN2s9GiM9qJCiBBlZtRTiEca+DC+xRPyCThTtWYgxjWHgU7ZkyvA==
"@next/swc-linux-arm64-musl@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.9.tgz#cee04c51610eddd3638ce2499205083656531ea0"
integrity sha512-OOn6zZBIVkm/4j5gkPdGn4yqQt+gmXaLaSjRSO434WplV8vo2YaBNbSHaTM9wJpZTHVDYyjzuIYVEzy9/5RVZw==
"@next/swc-linux-x64-gnu@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.19.tgz#96b0882492a2f7ffcce747846d3680730f69f4d1"
integrity sha512-htwOEagMa/CXNykFFeAHHvMJeqZfNQEoQvHfsA4wgg5QqGNqD5soeCer4oGlCol6NGUxknrQO6VEustcv+Md+g==
"@next/swc-linux-x64-gnu@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.9.tgz#1932d0367916adbc6844b244cda1d4182bd11f7a"
integrity sha512-iA+fJXFPpW0SwGmx/pivVU+2t4zQHNOOAr5T378PfxPHY6JtjV6/0s1vlAJUdIHeVpX98CLp9k5VuKgxiRHUpg==
"@next/swc-linux-x64-musl@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.19.tgz#f276b618afa321d2f7b17c81fc83f429fb0fd9d8"
integrity sha512-4Gj4vvtbK1JH8ApWTT214b3GwUh9EKKQjY41hH/t+u55Knxi/0wesMzwQRhppK6Ddalhu0TEttbiJ+wRcoEj5Q==
"@next/swc-linux-x64-musl@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.9.tgz#a66aa8c1383b16299b72482f6360facd5cde3c7a"
integrity sha512-rlNf2WUtMM+GAQrZ9gMNdSapkVi3koSW3a+dmBVp42lfugWVvnyzca/xJlN48/7AGx8qu62WyO0ya1ikgOxh6A==
"@next/swc-win32-arm64-msvc@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.19.tgz#1599ae0d401da5ffca0947823dac577697cce577"
integrity sha512-bUfDevQK4NsIAHXs3/JNgnvEY+LRyneDN788W2NYiRIIzmILjba7LaQTfihuFawZDhRtkYCv3JDC3B4TwnmRJw==
"@next/swc-win32-arm64-msvc@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.9.tgz#39482ee856c867177a612a30b6861c75e0736a4a"
integrity sha512-5T9ybSugXP77nw03vlgKZxD99AFTHaX8eT1ayKYYnGO9nmYhJjRPxcjU5FyYI+TdkQgEpIcH7p/guPLPR0EbKA==
"@next/swc-win32-ia32-msvc@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.19.tgz#55cdd7da90818f03e4da16d976f0cb22045d16fd"
integrity sha512-Y5kikILFAr81LYIFaw6j/NrOtmiM4Sf3GtOc0pn50ez2GCkr+oejYuKGcwAwq3jiTKuzF6OF4iT2INPoxRycEA==
"@next/swc-win32-ia32-msvc@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.9.tgz#29db85e34b597ade1a918235d16a760a9213c190"
integrity sha512-ojZTCt1lP2ucgpoiFgrFj07uq4CZsq4crVXpLGgQfoFq00jPKRPgesuGPaz8lg1yLfvafkU3Jd1i8snKwYR3LA==
"@next/swc-win32-x64-msvc@13.4.19":
version "13.4.19"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.19.tgz#648f79c4e09279212ac90d871646ae12d80cdfce"
integrity sha512-YzA78jBDXMYiINdPdJJwGgPNT3YqBNNGhsthsDoWHL9p24tEJn9ViQf/ZqTbwSpX/RrkPupLfuuTH2sf73JBAw==
"@next/swc-win32-x64-msvc@13.4.9":
version "13.4.9"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.9.tgz#0c2758164cccd61bc5a1c6cd8284fe66173e4a2b"
integrity sha512-QbT03FXRNdpuL+e9pLnu+XajZdm/TtIXVYY4lA9t+9l0fLZbHXDYEKitAqxrOj37o3Vx5ufxiRAniaIebYDCgw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
@@ -1221,10 +1221,10 @@
tiny-glob "^0.2.9"
tslib "^2.4.0"
"@remix-run/router@1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498"
integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ==
"@remix-run/router@1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.8.0.tgz#e848d2f669f601544df15ce2a313955e4bf0bafc"
integrity sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==
"@rushstack/eslint-patch@^1.1.3":
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"
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"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
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"
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
emoji-picker-react@^4.4.7:
version "4.4.8"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.4.8.tgz#cd18e942720d0d01e3d488a008f5e79aa315ec87"
integrity sha512-5bbj0PCvpjB64PZj31wZ35EoebF2mKoHqEEx9u2ZLghx7sGoD1MgyDhse851rqROypjhmK9IUY15QBa7mCLP0g==
emoji-picker-react@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.5.1.tgz#341f27dc86ad09340a316e0632484fcb9aff7195"
integrity sha512-zpm0ui0TWkXZDUIevyNM0rC9Jyqc08RvVXH0KgsbSkDr+VgMQmYLu6UeI4SIWMZKsKMjQwujPpncRCFlEeykjw==
dependencies:
clsx "^1.2.1"
@@ -3050,40 +3050,40 @@ eslint-scope@5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-scope@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
eslint-scope@^7.2.2:
version "7.2.2"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint@^8.44.0:
version "8.44.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.44.0.tgz#51246e3889b259bbcd1d7d736a0c10add4f0e500"
integrity sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==
eslint@^8.49.0:
version "8.49.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.49.0.tgz#09d80a89bdb4edee2efcf6964623af1054bf6d42"
integrity sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.4.0"
"@eslint/eslintrc" "^2.1.0"
"@eslint/js" "8.44.0"
"@humanwhocodes/config-array" "^0.11.10"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.2"
"@eslint/js" "8.49.0"
"@humanwhocodes/config-array" "^0.11.11"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
ajv "^6.10.0"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.2.0"
eslint-visitor-keys "^3.4.1"
espree "^9.6.0"
eslint-scope "^7.2.2"
eslint-visitor-keys "^3.4.3"
espree "^9.6.1"
esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
@@ -3093,7 +3093,6 @@ eslint@^8.44.0:
globals "^13.19.0"
graphemer "^1.4.0"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
@@ -3105,13 +3104,12 @@ eslint@^8.44.0:
natural-compare "^1.4.0"
optionator "^0.9.3"
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
espree@^9.6.0:
version "9.6.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f"
integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
dependencies:
acorn "^8.9.0"
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"
integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==
import-fresh@^3.0.0, import-fresh@^3.2.1:
import-fresh@^3.2.1:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -4707,12 +4705,12 @@ neo-async@^2.6.2:
resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next@^13.4.19:
version "13.4.19"
resolved "https://registry.yarnpkg.com/next/-/next-13.4.19.tgz#2326e02aeedee2c693d4f37b90e4f0ed6882b35f"
integrity sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==
next@^13.4.9:
version "13.4.9"
resolved "https://registry.yarnpkg.com/next/-/next-13.4.9.tgz#473de5997cb4c5d7a4fb195f566952a1cbffbeba"
integrity sha512-vtefFm/BWIi/eWOqf1GsmKG3cjKw1k3LjuefKRcL3iiLl3zWzFdPG3as6xtxrGO6gwTzzaO1ktL4oiHt/uvTjA==
dependencies:
"@next/env" "13.4.19"
"@next/env" "13.4.9"
"@swc/helpers" "0.5.1"
busboy "1.6.0"
caniuse-lite "^1.0.30001406"
@@ -4721,15 +4719,15 @@ next@^13.4.19:
watchpack "2.4.0"
zod "3.21.4"
optionalDependencies:
"@next/swc-darwin-arm64" "13.4.19"
"@next/swc-darwin-x64" "13.4.19"
"@next/swc-linux-arm64-gnu" "13.4.19"
"@next/swc-linux-arm64-musl" "13.4.19"
"@next/swc-linux-x64-gnu" "13.4.19"
"@next/swc-linux-x64-musl" "13.4.19"
"@next/swc-win32-arm64-msvc" "13.4.19"
"@next/swc-win32-ia32-msvc" "13.4.19"
"@next/swc-win32-x64-msvc" "13.4.19"
"@next/swc-darwin-arm64" "13.4.9"
"@next/swc-darwin-x64" "13.4.9"
"@next/swc-linux-arm64-gnu" "13.4.9"
"@next/swc-linux-arm64-musl" "13.4.9"
"@next/swc-linux-x64-gnu" "13.4.9"
"@next/swc-linux-x64-musl" "13.4.9"
"@next/swc-win32-arm64-msvc" "13.4.9"
"@next/swc-win32-ia32-msvc" "13.4.9"
"@next/swc-win32-x64-msvc" "13.4.9"
node-domexception@^1.0.0:
version "1.0.0"
@@ -5092,20 +5090,20 @@ react-redux@^8.1.1:
react-is "^18.0.0"
use-sync-external-store "^1.0.0"
react-router-dom@^6.14.1:
version "6.14.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f"
integrity sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw==
react-router-dom@^6.15.0:
version "6.15.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.15.0.tgz#6da7db61e56797266fbbef0d5e324d6ac443ee40"
integrity sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==
dependencies:
"@remix-run/router" "1.7.1"
react-router "6.14.1"
"@remix-run/router" "1.8.0"
react-router "6.15.0"
react-router@6.14.1:
version "6.14.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810"
integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g==
react-router@6.15.0:
version "6.15.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.15.0.tgz#bf2cb5a4a7ed57f074d4ea88db0d95033f39cac8"
integrity sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==
dependencies:
"@remix-run/router" "1.7.1"
"@remix-run/router" "1.8.0"
react@^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"
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"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
@@ -5786,10 +5784,10 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"
typescript@4.9.5:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typescript@5.2.2:
version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
unbox-primitive@^1.0.2:
version "1.0.2"