Compare commits

...

63 Commits

Author SHA1 Message Date
Yifei Zhang
f21f922160 Update tauri.conf.json 2023-10-09 22:46:36 +08:00
Yifei Zhang
fd413c7b52 Merge pull request #2947 from KeithHello/main 2023-10-09 22:45:56 +08:00
Yifei Zhang
3e2c5af4b5 Merge pull request #2975 from luckykong/patch-1 2023-10-09 22:38:45 +08:00
Kong Gaowen
bdb49b1171 修改界面
似乎这里引入了一个逻辑判断错误,会导致打包之后的界面变宽变高。
2023-10-09 18:50:10 +08:00
GH Action - Upstream Sync
5933b3d7eb Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web 2023-10-08 00:19:29 +00:00
Yifei Zhang
4c8d606fae Merge pull request #2950 from PeterDaveHello/locale-tw 2023-10-08 00:20:39 +08:00
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
Peter Dave Hello
88f8c43472 Improve tw locale 2023-10-07 23:06:20 +08:00
Yifei Zhang
ef3e8e6fac Merge pull request #2933 from H0llyW00dzZ/authpage 2023-10-07 23:03:56 +08:00
KeithHello
1505372e20 Change log
-   config.ts
    -   line 72: remove type confirmation of x as x always has type 'number'
    -   line 135: remove the redundant local variable
-   chat.ts
    -   delete the unused import
2023-10-04 18:08:29 +09:00
KeithHello
ad5093ce05 Change log
-   config.ts
    -   line 72 remove type confirmation of x as x always has type 'number'
    -   line 135 remove redundant local variable
-   chat.ts
    -   delete unused import
2023-10-04 17:40:23 +09: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
31 changed files with 624 additions and 235 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://chatgpt2.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

@@ -26,13 +26,18 @@ async function handle(
duplex: "half",
};
console.log("[Any Proxy]", targetUrl);
const fetchResult = await fetch(targetUrl, fetchOptions);
const fetchResult = 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 = "edge";
export const runtime = "nodejs";

View File

@@ -15,7 +15,8 @@ export function AuthPage() {
const access = useAccessStore();
const goHome = () => navigate(Path.Home);
const resetAccessCode = () => access.updateCode(""); // Reset access code to empty string
const goChat = () => navigate(Path.Chat);
const resetAccessCode = () => { access.updateCode(""); access.updateToken(""); }; // Reset access code to empty string
useEffect(() => {
if (getClientConfig()?.isApp) {
@@ -42,17 +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={() => {
resetAccessCode();
goHome();
}} />
</div>
</div>
);

View File

@@ -937,7 +937,7 @@ function _Chat() {
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
const isHitBottom =
bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10);
bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
@@ -1155,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

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

@@ -50,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";
@@ -275,7 +275,7 @@ function CheckButton() {
return (
<IconButton
text="检查可用性"
text={Locale.Settings.Sync.Config.Modal.Check}
bordered
onClick={check}
icon={
@@ -413,7 +413,42 @@ function SyncConfigModal(props: { onClose?: () => void }) {
{syncStore.provider === ProviderType.UpStash && (
<List>
<ListItem title={Locale.WIP}></ListItem>
<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>

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

@@ -8,7 +8,7 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
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_CORS_HOST = "https://chatgpt2.nextweb.fun";
export const DEFAULT_CORS_HOST = "https://nb.nextweb.fun";
export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
export enum Path {
@@ -43,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;

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

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: "稍后再说",
@@ -187,6 +188,7 @@ const cn = {
Config: {
Modal: {
Title: "配置云同步",
Check: "检查可用性",
},
SyncType: {
Title: "同步类型",
@@ -206,6 +208,12 @@ const cn = {
UserName: "用户名",
Password: "密码",
},
UpStash: {
Endpoint: "UpStash Redis REST Url",
UserName: "备份名称",
Password: "UpStash Redis REST Token",
},
},
LocalState: "本地数据",
@@ -316,6 +324,10 @@ const cn = {
Success: "已写入剪切板",
Failed: "复制失败,请赋予剪切板权限",
},
Download: {
Success: "内容已下载到您的目录。",
Failed: "下载失败。",
},
Context: {
Toast: (x: any) => `包含 ${x} 条预设提示词`,
Edit: "当前对话设置",

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",
@@ -189,6 +190,7 @@ const en: LocaleType = {
Config: {
Modal: {
Title: "Config Sync",
Check: "Check Connection",
},
SyncType: {
Title: "Sync Type",
@@ -209,6 +211,12 @@ const en: LocaleType = {
UserName: "User Name",
Password: "Password",
},
UpStash: {
Endpoint: "UpStash Redis REST Url",
UserName: "Backup Name",
Password: "UpStash Redis REST Token",
},
},
LocalState: "Local Data",
@@ -322,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",

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

@@ -7,13 +7,13 @@ const tw: PartialLocaleType = {
Unauthorized: "目前您的狀態是未授權,請前往[設定頁面](/#/auth)輸入授權碼。",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} 對話`,
ChatItemCount: (count: number) => `${count} 對話`,
},
Chat: {
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 對話`,
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 對話`,
Actions: {
ChatList: "查看訊息列表",
CompressedHistory: "查看壓縮後的歷史 Prompt",
ChatList: "檢視訊息列表",
CompressedHistory: "檢視壓縮後的歷史 Prompt",
Export: "匯出聊天紀錄",
Copy: "複製",
Stop: "停止",
@@ -23,15 +23,15 @@ const tw: PartialLocaleType = {
Rename: "重新命名對話",
Typing: "正在輸入…",
Input: (submitKey: string) => {
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可`;
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += "Shift + Enter 鍵換行";
}
return inputHints;
},
Send: "送",
Send: "送",
Config: {
Reset: "重置預設",
Reset: "重設",
SaveAs: "另存新檔",
},
},
@@ -46,7 +46,7 @@ const tw: PartialLocaleType = {
Title: "上下文記憶 Prompt",
EmptyContent: "尚未記憶",
Copy: "複製全部",
Send: "送記憶",
Send: "送記憶",
Reset: "重設對話",
ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?",
},
@@ -71,22 +71,22 @@ const tw: PartialLocaleType = {
},
InjectSystemPrompts: {
Title: "匯入系統提示",
SubTitle: "強制在每個請求的訊息列表開頭添加一個模擬 ChatGPT 的系統提示",
SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
},
Update: {
Version: (x: string) => `前版本:${x}`,
Version: (x: string) => `前版本:${x}`,
IsLatest: "已是最新版本",
CheckUpdate: "檢查更新",
IsChecking: "正在檢查更新...",
FoundUpdate: (x: string) => `發現新版本:${x}`,
GoToUpdate: "前往更新",
},
SendKey: "送鍵",
SendKey: "送鍵",
Theme: "主題",
TightBorder: "緊湊邊框",
SendPreviewBubble: {
Title: "預覽氣泡",
SubTitle: "在預覽氣泡中預覽 Markdown 容",
SubTitle: "在預覽氣泡中預覽 Markdown 容",
},
Mask: {
Splash: {
@@ -101,7 +101,7 @@ const tw: PartialLocaleType = {
},
List: "自定義提示詞列表",
ListCount: (builtin: number, custom: number) =>
`內建 ${builtin} 條,用戶定義 ${custom}`,
`內建 ${builtin} 條,使用者定義 ${custom}`,
Edit: "編輯",
Modal: {
Title: "提示詞列表",
@@ -132,7 +132,7 @@ const tw: PartialLocaleType = {
},
IsChecking: "正在檢查…",
Check: "重新檢查",
NoAccess: "輸入API Key查看餘額",
NoAccess: "輸入 API Key 檢視餘額",
},
AccessCode: {
Title: "授權碼",
@@ -150,7 +150,7 @@ const tw: PartialLocaleType = {
},
PresencePenalty: {
Title: "話題新穎度 (presence_penalty)",
SubTitle: "值越大,越有可能展到新話題",
SubTitle: "值越大,越有可能展到新話題",
},
FrequencyPenalty: {
Title: "頻率懲罰度 (frequency_penalty)",
@@ -163,7 +163,7 @@ const tw: PartialLocaleType = {
Error: "出錯了,請稍後再嘗試",
Prompt: {
History: (content: string) =>
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
"這是 AI 與使用者的歷史聊天總結,作為前情提要:" + content,
Topic:
"Use the language used by the user (e.g. en for english conversation, zh-hant for chinese conversation, etc.) to generate a title (at most 6 words) summarizing our conversation without any lead-in, quotation marks, preamble like 'Title:', direct text copies, single-word replies, quotation marks, translations, or brackets. Remove enclosing quotation marks. The title should make third-party grasp the essence of the conversation in first sight.",
Summarize:
@@ -192,16 +192,16 @@ const tw: PartialLocaleType = {
Item: {
Info: (count: number) => `包含 ${count} 條預設對話`,
Chat: "對話",
View: "查看",
View: "檢視",
Edit: "編輯",
Delete: "除",
DeleteConfirm: "確認除?",
Delete: "除",
DeleteConfirm: "確認除?",
},
EditModal: {
Title: (readonly: boolean) =>
`編輯預設面具 ${readonly ? "(只" : ""}`,
`編輯預設面具 ${readonly ? "(只" : ""}`,
Download: "下載預設",
Clone: "克隆預設",
Clone: "複製預設",
},
Config: {
Avatar: "角色頭像",
@@ -215,7 +215,7 @@ const tw: PartialLocaleType = {
SubTitle: "現在開始,與面具背後的靈魂思維碰撞",
More: "搜尋更多",
NotShow: "不再呈現",
ConfirmNoShow: "確認用?用後可以時在設定中重新啟用。",
ConfirmNoShow: "確認用?用後可以時在設定中重新啟用。",
},
UI: {
Confirm: "確認",

View File

@@ -1,6 +1,3 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { trimTopic } from "../utils";
import Locale, { getLang } from "../locales";

View File

@@ -1,6 +1,12 @@
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"];
@@ -22,14 +28,14 @@ export enum Theme {
export const DEFAULT_CONFIG = {
lastUpdate: Date.now(), // timestamp, to merge state
submitKey: SubmitKey.CtrlEnter as SubmitKey,
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,
@@ -64,7 +70,7 @@ export function limitNumber(
max: number,
defaultValue: number,
) {
if (typeof x !== "number" || isNaN(x)) {
if (isNaN(x)) {
return defaultValue;
}
@@ -127,9 +133,7 @@ export const useAppConfig = createPersistStore(
.customModels.split(",")
.filter((v) => !!v && v.length > 0)
.map((m) => ({ name: m, available: true }));
const models = get().models.concat(customModels);
return models;
return get().models.concat(customModels);
},
}),
{

View File

@@ -1,5 +1,6 @@
import { getClientConfig } from "../config/client";
import { Updater } from "../typing";
import { ApiPath, StoreKey } from "../constant";
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
import { createPersistStore } from "../utils/store";
import {
AppState,
@@ -20,29 +21,32 @@ export interface WebDavConfig {
password: string;
}
const isApp = !!getClientConfig()?.isApp;
export type SyncStore = GetStoreState<typeof useSyncStore>;
export const useSyncStore = createPersistStore(
{
provider: ProviderType.WebDAV,
useProxy: true,
proxyUrl: corsPath(ApiPath.Cors),
const DEFAULT_SYNC_STATE = {
provider: ProviderType.WebDAV,
useProxy: true,
proxyUrl: corsPath(ApiPath.Cors),
webdav: {
endpoint: "",
username: "",
password: "",
},
upstash: {
endpoint: "",
username: "",
apiKey: "",
},
lastSyncTime: 0,
lastProvider: "",
webdav: {
endpoint: "",
username: "",
password: "",
},
upstash: {
endpoint: "",
username: STORAGE_KEY,
apiKey: "",
},
lastSyncTime: 0,
lastProvider: "",
};
export const useSyncStore = createPersistStore(
DEFAULT_SYNC_STATE,
(set, get) => ({
coundSync() {
const config = get()[get().provider];
@@ -55,7 +59,11 @@ export const useSyncStore = createPersistStore(
export() {
const state = getLocalAppState();
const fileName = `Backup-${new Date().toLocaleString()}.json`;
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);
},
@@ -108,6 +116,16 @@ export const useSyncStore = createPersistStore(
}),
{
name: StoreKey.Sync,
version: 1,
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

@@ -2,8 +2,11 @@ import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
import { api } from "../client/api";
import { getClientConfig } from "../config/client";
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);
@@ -80,6 +83,38 @@ export const useUpdateStore = createPersistStore(
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);

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
}

View File

@@ -1,25 +1,87 @@
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(config: UpstashConfig) {
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() {
return true;
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() {
throw Error("[Sync] not implemented");
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() {
throw Error("[Sync] not implemented");
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: `Basic ${config.apiKey}`,
Authorization: `Bearer ${config.apiKey}`,
};
},
path(path: string) {

View File

@@ -20,10 +20,8 @@ export function createWebDavClient(store: SyncStore) {
headers: this.headers(),
proxyUrl,
});
console.log("[WebDav] check", res.status, res.statusText);
return [201, 200, 404].includes(res.status);
return [201, 200, 404, 301, 302, 307, 308].includes(res.status);
} catch (e) {
console.error("[WebDav] failed to check", e);
}

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

View File

@@ -69,6 +69,9 @@ const MergeStates: StateMerger = {
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

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.6"
"version": "2.9.9"
},
"tauri": {
"allowlist": {
@@ -44,6 +44,12 @@
"startDragging": true,
"unmaximize": true,
"unminimize": true
},
"fs": {
"all": true
},
"notification": {
"all": true
}
},
"bundle": {