mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-01 12:46:58 +08:00
Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
13c1d2fd2b | ||
|
e35c807216 | ||
|
5a2cc6f154 | ||
|
ef3e8e6fac | ||
|
b558d1afc6 | ||
|
ddfd05b008 | ||
|
d2ad01a9ff | ||
|
64a17abfe2 | ||
|
04b638aa06 | ||
|
31e30906d0 | ||
|
bc00be9065 | ||
|
4a599e986f | ||
|
f1ca03e378 | ||
|
f3d5fc7a84 | ||
|
3bfcdf9c41 | ||
|
144200e315 | ||
|
398e229c77 | ||
|
6a61fe5776 | ||
|
9835206452 | ||
|
70b0580fb7 | ||
|
23eb7732d7 | ||
|
26e50cefea | ||
|
588e907181 | ||
|
eae7d6260f | ||
|
175b4e7f92 | ||
|
b050417ab1 | ||
|
37b49400db | ||
|
ebcb2e7837 | ||
|
f1e7db6a88 | ||
|
2ba0929458 | ||
|
83fed42997 | ||
|
2c4626709c | ||
|
59fbadd9eb | ||
|
adb860b464 | ||
|
8d8790586d | ||
|
61ca60c550 | ||
|
d713d01600 | ||
|
372ea0f845 | ||
|
61888708f5 | ||
|
c900459f73 | ||
|
2c92f75c86 | ||
|
3e1514239c | ||
|
0707a1d49a | ||
|
9521f19507 | ||
|
bd69116df2 | ||
|
6535986484 | ||
|
e03db9c2d5 | ||
|
038790370c | ||
|
261bf0b298 | ||
|
5a7bdcfe59 | ||
|
4f3261b262 | ||
|
48e6087b1b | ||
|
28103c901d | ||
|
368701610f |
@@ -15,7 +15,6 @@ BASE_URL=
|
|||||||
|
|
||||||
# Specify OpenAI organization ID.(optional)
|
# Specify OpenAI organization ID.(optional)
|
||||||
# Default: Empty
|
# Default: Empty
|
||||||
# If you do not want users to input their own API key, set this value to 1.
|
|
||||||
OPENAI_ORG_ID=
|
OPENAI_ORG_ID=
|
||||||
|
|
||||||
# (optional)
|
# (optional)
|
||||||
@@ -31,4 +30,4 @@ DISABLE_GPT4=
|
|||||||
# (optional)
|
# (optional)
|
||||||
# Default: Empty
|
# Default: Empty
|
||||||
# If you do not want users to query balance, set this value to 1.
|
# If you do not want users to query balance, set this value to 1.
|
||||||
HIDE_BALANCE_QUERY=
|
HIDE_BALANCE_QUERY=
|
||||||
|
@@ -114,7 +114,7 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
|||||||
OPENAI_API_KEY=<your api key here>
|
OPENAI_API_KEY=<your api key here>
|
||||||
|
|
||||||
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
# 中国大陆用户,可以使用本项目自带的代理进行开发,你也可以自由选择其他代理地址
|
||||||
BASE_URL=https://chatgpt2.nextweb.fun/api/proxy
|
BASE_URL=https://nb.nextweb.fun/api/proxy
|
||||||
```
|
```
|
||||||
|
|
||||||
### 本地开发
|
### 本地开发
|
||||||
|
@@ -4,7 +4,7 @@ import { getServerSideConfig } from "../../config/server";
|
|||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
// Danger! Don not write any secret value here!
|
// Danger! Do not hard code any secret value here!
|
||||||
// 警告!不要在这里写入任何敏感信息!
|
// 警告!不要在这里写入任何敏感信息!
|
||||||
const DANGER_CONFIG = {
|
const DANGER_CONFIG = {
|
||||||
needCode: serverConfig.needCode,
|
needCode: serverConfig.needCode,
|
||||||
|
@@ -26,13 +26,18 @@ async function handle(
|
|||||||
duplex: "half",
|
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;
|
return fetchResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST = handle;
|
export const POST = handle;
|
||||||
|
export const GET = handle;
|
||||||
|
export const OPTIONS = handle;
|
||||||
|
|
||||||
export const runtime = "edge";
|
export const runtime = "nodejs";
|
||||||
|
@@ -15,7 +15,8 @@ export function AuthPage() {
|
|||||||
const access = useAccessStore();
|
const access = useAccessStore();
|
||||||
|
|
||||||
const goHome = () => navigate(Path.Home);
|
const goHome = () => navigate(Path.Home);
|
||||||
const resetAccessCode = () => access.updateCode(""); // Reset access code to empty string
|
const goChat = () => navigate(Path.Chat);
|
||||||
|
const resetAccessCode = () => { access.updateCode(""); access.updateToken(""); }; // Reset access code to empty string
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (getClientConfig()?.isApp) {
|
if (getClientConfig()?.isApp) {
|
||||||
@@ -42,17 +43,34 @@ export function AuthPage() {
|
|||||||
access.updateCode(e.currentTarget.value);
|
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"]}>
|
<div className={styles["auth-actions"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
text={Locale.Auth.Confirm}
|
text={Locale.Auth.Confirm}
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={goHome}
|
onClick={goChat}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
text={Locale.Auth.Later}
|
||||||
|
onClick={() => {
|
||||||
|
resetAccessCode();
|
||||||
|
goHome();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton text={Locale.Auth.Later} onClick={() => {
|
|
||||||
resetAccessCode();
|
|
||||||
goHome();
|
|
||||||
}} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -937,7 +937,7 @@ function _Chat() {
|
|||||||
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
|
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
|
||||||
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
|
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
|
||||||
const isHitBottom =
|
const isHitBottom =
|
||||||
bottomHeight >= e.scrollHeight - (isMobileScreen ? 0 : 10);
|
bottomHeight >= e.scrollHeight - (isMobileScreen ? 4 : 10);
|
||||||
|
|
||||||
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
|
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
|
||||||
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
|
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
|
||||||
@@ -1155,7 +1155,13 @@ function _Chat() {
|
|||||||
{isUser ? (
|
{isUser ? (
|
||||||
<Avatar avatar={config.avatar} />
|
<Avatar avatar={config.avatar} />
|
||||||
) : (
|
) : (
|
||||||
<MaskAvatar mask={session.mask} />
|
<>
|
||||||
|
{["system"].includes(message.role) ? (
|
||||||
|
<Avatar avatar="2699-fe0f" />
|
||||||
|
) : (
|
||||||
|
<MaskAvatar mask={session.mask} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -433,25 +433,55 @@ export function ImagePreviewer(props: {
|
|||||||
|
|
||||||
const isMobile = useMobileScreen();
|
const isMobile = useMobileScreen();
|
||||||
|
|
||||||
const download = () => {
|
const download = async () => {
|
||||||
showToast(Locale.Export.Image.Toast);
|
showToast(Locale.Export.Image.Toast);
|
||||||
const dom = previewRef.current;
|
const dom = previewRef.current;
|
||||||
if (!dom) return;
|
if (!dom) return;
|
||||||
toPng(dom)
|
|
||||||
.then((blob) => {
|
const isApp = getClientConfig()?.isApp;
|
||||||
if (!blob) return;
|
|
||||||
|
try {
|
||||||
if (isMobile || getClientConfig()?.isApp) {
|
const blob = await toPng(dom);
|
||||||
showImageModal(blob);
|
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 {
|
} else {
|
||||||
const link = document.createElement("a");
|
showImageModal(blob);
|
||||||
link.download = `${props.topic}.png`;
|
|
||||||
link.href = blob;
|
|
||||||
link.click();
|
|
||||||
refreshPreview();
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
.catch((e) => console.log("[Export Image] ", e));
|
const link = document.createElement("a");
|
||||||
|
link.download = `${props.topic}.png`;
|
||||||
|
link.href = blob;
|
||||||
|
link.click();
|
||||||
|
refreshPreview();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast(Locale.Download.Failed);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshPreview = () => {
|
const refreshPreview = () => {
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
color: var(--black);
|
color: var(--black);
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
min-height: 480px;
|
min-height: 370px;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@@ -115,7 +115,10 @@ const loadAsyncGoogleFont = () => {
|
|||||||
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
|
getClientConfig()?.buildMode === "export" ? remoteFontUrl : proxyFontUrl;
|
||||||
linkEl.rel = "stylesheet";
|
linkEl.rel = "stylesheet";
|
||||||
linkEl.href =
|
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);
|
document.head.appendChild(linkEl);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,6 +128,8 @@ function Screen() {
|
|||||||
const isHome = location.pathname === Path.Home;
|
const isHome = location.pathname === Path.Home;
|
||||||
const isAuth = location.pathname === Path.Auth;
|
const isAuth = location.pathname === Path.Auth;
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
|
const shouldTightBorder =
|
||||||
|
config.tightBorder && !isMobileScreen && !getClientConfig()?.isApp;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAsyncGoogleFont();
|
loadAsyncGoogleFont();
|
||||||
@@ -134,11 +139,9 @@ function Screen() {
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
styles.container +
|
styles.container +
|
||||||
` ${
|
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
|
||||||
config.tightBorder && !isMobileScreen
|
getLang() === "ar" ? styles["rtl-screen"] : ""
|
||||||
? styles["tight-container"]
|
}`
|
||||||
: styles.container
|
|
||||||
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isAuth ? (
|
{isAuth ? (
|
||||||
|
@@ -151,6 +151,7 @@ export function Markdown(
|
|||||||
ref={mdRef}
|
ref={mdRef}
|
||||||
onContextMenu={props.onContextMenu}
|
onContextMenu={props.onContextMenu}
|
||||||
onDoubleClickCapture={props.onDoubleClickCapture}
|
onDoubleClickCapture={props.onDoubleClickCapture}
|
||||||
|
dir="auto"
|
||||||
>
|
>
|
||||||
{props.loading ? (
|
{props.loading ? (
|
||||||
<LoadingIcon />
|
<LoadingIcon />
|
||||||
|
@@ -50,7 +50,7 @@ import Locale, {
|
|||||||
} from "../locales";
|
} from "../locales";
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
import Link from "next/link";
|
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 { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||||
import { ErrorBoundary } from "./error";
|
import { ErrorBoundary } from "./error";
|
||||||
import { InputRange } from "./input-range";
|
import { InputRange } from "./input-range";
|
||||||
@@ -275,7 +275,7 @@ function CheckButton() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
text="检查可用性"
|
text={Locale.Settings.Sync.Config.Modal.Check}
|
||||||
bordered
|
bordered
|
||||||
onClick={check}
|
onClick={check}
|
||||||
icon={
|
icon={
|
||||||
@@ -413,7 +413,42 @@ function SyncConfigModal(props: { onClose?: () => void }) {
|
|||||||
|
|
||||||
{syncStore.provider === ProviderType.UpStash && (
|
{syncStore.provider === ProviderType.UpStash && (
|
||||||
<List>
|
<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>
|
</List>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useCallback } from "react";
|
||||||
|
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
@@ -17,6 +17,7 @@ import Locale from "../locales";
|
|||||||
import { useAppConfig, useChatStore } from "../store";
|
import { useAppConfig, useChatStore } from "../store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_SIDEBAR_WIDTH,
|
||||||
MAX_SIDEBAR_WIDTH,
|
MAX_SIDEBAR_WIDTH,
|
||||||
MIN_SIDEBAR_WIDTH,
|
MIN_SIDEBAR_WIDTH,
|
||||||
NARROW_SIDEBAR_WIDTH,
|
NARROW_SIDEBAR_WIDTH,
|
||||||
@@ -57,31 +58,57 @@ function useDragSideBar() {
|
|||||||
|
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const startX = useRef(0);
|
const startX = useRef(0);
|
||||||
const startDragWidth = useRef(config.sidebarWidth ?? 300);
|
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||||
const lastUpdateTime = useRef(Date.now());
|
const lastUpdateTime = useRef(Date.now());
|
||||||
|
|
||||||
const handleMouseMove = useRef((e: MouseEvent) => {
|
const toggleSideBar = () => {
|
||||||
if (Date.now() < lastUpdateTime.current + 50) {
|
config.update((config) => {
|
||||||
return;
|
if (config.sidebarWidth < MIN_SIDEBAR_WIDTH) {
|
||||||
}
|
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||||
lastUpdateTime.current = Date.now();
|
} else {
|
||||||
const d = e.clientX - startX.current;
|
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||||
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 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 isMobileScreen = useMobileScreen();
|
||||||
const shouldNarrow =
|
const shouldNarrow =
|
||||||
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
!isMobileScreen && config.sidebarWidth < MIN_SIDEBAR_WIDTH;
|
||||||
@@ -89,13 +116,13 @@ function useDragSideBar() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const barWidth = shouldNarrow
|
const barWidth = shouldNarrow
|
||||||
? NARROW_SIDEBAR_WIDTH
|
? NARROW_SIDEBAR_WIDTH
|
||||||
: limit(config.sidebarWidth ?? 300);
|
: limit(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||||
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
|
||||||
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
|
||||||
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
}, [config.sidebarWidth, isMobileScreen, shouldNarrow]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDragMouseDown,
|
onDragStart,
|
||||||
shouldNarrow,
|
shouldNarrow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -104,7 +131,7 @@ export function SideBar(props: { className?: string }) {
|
|||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
// drag side bar
|
// drag side bar
|
||||||
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
|
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
@@ -133,7 +160,13 @@ export function SideBar(props: { className?: string }) {
|
|||||||
icon={<MaskIcon />}
|
icon={<MaskIcon />}
|
||||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||||
className={styles["sidebar-bar-button"]}
|
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
|
shadow
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -198,7 +231,7 @@ export function SideBar(props: { className?: string }) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles["sidebar-drag"]}
|
className={styles["sidebar-drag"]}
|
||||||
onMouseDown={(e) => onDragMouseDown(e as any)}
|
onPointerDown={(e) => onDragStart(e as any)}
|
||||||
>
|
>
|
||||||
<DragIcon />
|
<DragIcon />
|
||||||
</div>
|
</div>
|
||||||
|
@@ -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 FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||||
|
|
||||||
export const DEFAULT_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 const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`;
|
||||||
|
|
||||||
export enum Path {
|
export enum Path {
|
||||||
@@ -43,6 +43,7 @@ export enum StoreKey {
|
|||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||||
export const MAX_SIDEBAR_WIDTH = 500;
|
export const MAX_SIDEBAR_WIDTH = 500;
|
||||||
export const MIN_SIDEBAR_WIDTH = 230;
|
export const MIN_SIDEBAR_WIDTH = 230;
|
||||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||||
|
12
app/global.d.ts
vendored
12
app/global.d.ts
vendored
@@ -13,5 +13,17 @@ declare module "*.svg";
|
|||||||
declare interface Window {
|
declare interface Window {
|
||||||
__TAURI__?: {
|
__TAURI__?: {
|
||||||
writeText(text: string): Promise<void>;
|
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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,7 @@ const ar: PartialLocaleType = {
|
|||||||
Auth: {
|
Auth: {
|
||||||
Title: "تحتاج إلى رمز الوصول",
|
Title: "تحتاج إلى رمز الوصول",
|
||||||
Tips: "يرجى إدخال رمز الوصول أدناه",
|
Tips: "يرجى إدخال رمز الوصول أدناه",
|
||||||
|
SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك",
|
||||||
Input: "رمز الوصول",
|
Input: "رمز الوصول",
|
||||||
Confirm: "تأكيد",
|
Confirm: "تأكيد",
|
||||||
Later: "لاحقًا",
|
Later: "لاحقًا",
|
||||||
|
@@ -10,6 +10,7 @@ const bn: PartialLocaleType = {
|
|||||||
Auth: {
|
Auth: {
|
||||||
Title: "একটি অ্যাক্সেস কোড প্রয়োজন",
|
Title: "একটি অ্যাক্সেস কোড প্রয়োজন",
|
||||||
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন",
|
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন",
|
||||||
|
SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করুন",
|
||||||
Input: "অ্যাক্সেস কোড",
|
Input: "অ্যাক্সেস কোড",
|
||||||
Confirm: "নিশ্চিত করুন",
|
Confirm: "নিশ্চিত করুন",
|
||||||
Later: "পরে",
|
Later: "পরে",
|
||||||
|
@@ -13,6 +13,7 @@ const cn = {
|
|||||||
Auth: {
|
Auth: {
|
||||||
Title: "需要密码",
|
Title: "需要密码",
|
||||||
Tips: "管理员开启了密码验证,请在下方填入访问码",
|
Tips: "管理员开启了密码验证,请在下方填入访问码",
|
||||||
|
SubTips: "或者输入你的 OpenAI API 密钥",
|
||||||
Input: "在此处填写访问码",
|
Input: "在此处填写访问码",
|
||||||
Confirm: "确认",
|
Confirm: "确认",
|
||||||
Later: "稍后再说",
|
Later: "稍后再说",
|
||||||
@@ -187,6 +188,7 @@ const cn = {
|
|||||||
Config: {
|
Config: {
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "配置云同步",
|
Title: "配置云同步",
|
||||||
|
Check: "检查可用性",
|
||||||
},
|
},
|
||||||
SyncType: {
|
SyncType: {
|
||||||
Title: "同步类型",
|
Title: "同步类型",
|
||||||
@@ -206,6 +208,12 @@ const cn = {
|
|||||||
UserName: "用户名",
|
UserName: "用户名",
|
||||||
Password: "密码",
|
Password: "密码",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
UpStash: {
|
||||||
|
Endpoint: "UpStash Redis REST Url",
|
||||||
|
UserName: "备份名称",
|
||||||
|
Password: "UpStash Redis REST Token",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
LocalState: "本地数据",
|
LocalState: "本地数据",
|
||||||
@@ -316,6 +324,10 @@ const cn = {
|
|||||||
Success: "已写入剪切板",
|
Success: "已写入剪切板",
|
||||||
Failed: "复制失败,请赋予剪切板权限",
|
Failed: "复制失败,请赋予剪切板权限",
|
||||||
},
|
},
|
||||||
|
Download: {
|
||||||
|
Success: "内容已下载到您的目录。",
|
||||||
|
Failed: "下载失败。",
|
||||||
|
},
|
||||||
Context: {
|
Context: {
|
||||||
Toast: (x: any) => `包含 ${x} 条预设提示词`,
|
Toast: (x: any) => `包含 ${x} 条预设提示词`,
|
||||||
Edit: "当前对话设置",
|
Edit: "当前对话设置",
|
||||||
|
@@ -15,6 +15,7 @@ const en: LocaleType = {
|
|||||||
Auth: {
|
Auth: {
|
||||||
Title: "Need Access Code",
|
Title: "Need Access Code",
|
||||||
Tips: "Please enter access code below",
|
Tips: "Please enter access code below",
|
||||||
|
SubTips: "Or enter your OpenAI API Key",
|
||||||
Input: "access code",
|
Input: "access code",
|
||||||
Confirm: "Confirm",
|
Confirm: "Confirm",
|
||||||
Later: "Later",
|
Later: "Later",
|
||||||
@@ -189,6 +190,7 @@ const en: LocaleType = {
|
|||||||
Config: {
|
Config: {
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "Config Sync",
|
Title: "Config Sync",
|
||||||
|
Check: "Check Connection",
|
||||||
},
|
},
|
||||||
SyncType: {
|
SyncType: {
|
||||||
Title: "Sync Type",
|
Title: "Sync Type",
|
||||||
@@ -209,6 +211,12 @@ const en: LocaleType = {
|
|||||||
UserName: "User Name",
|
UserName: "User Name",
|
||||||
Password: "Password",
|
Password: "Password",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
UpStash: {
|
||||||
|
Endpoint: "UpStash Redis REST Url",
|
||||||
|
UserName: "Backup Name",
|
||||||
|
Password: "UpStash Redis REST Token",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
LocalState: "Local Data",
|
LocalState: "Local Data",
|
||||||
@@ -322,6 +330,10 @@ const en: LocaleType = {
|
|||||||
Success: "Copied to clipboard",
|
Success: "Copied to clipboard",
|
||||||
Failed: "Copy failed, please grant permission to access clipboard",
|
Failed: "Copy failed, please grant permission to access clipboard",
|
||||||
},
|
},
|
||||||
|
Download: {
|
||||||
|
Success: "Content downloaded to your directory.",
|
||||||
|
Failed: "Download failed.",
|
||||||
|
},
|
||||||
Context: {
|
Context: {
|
||||||
Toast: (x: any) => `With ${x} contextual prompts`,
|
Toast: (x: any) => `With ${x} contextual prompts`,
|
||||||
Edit: "Current Chat Settings",
|
Edit: "Current Chat Settings",
|
||||||
|
@@ -4,12 +4,12 @@ import { PartialLocaleType } from "./index";
|
|||||||
const id: PartialLocaleType = {
|
const id: PartialLocaleType = {
|
||||||
WIP: "Coming Soon...",
|
WIP: "Coming Soon...",
|
||||||
Error: {
|
Error: {
|
||||||
Unauthorized:
|
Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
|
||||||
"Akses tidak diizinkan. Silakan [otorisasi](/#/auth) dengan memasukkan kode akses.",
|
},
|
||||||
},
|
|
||||||
Auth: {
|
Auth: {
|
||||||
Title: "Diperlukan Kode Akses",
|
Title: "Diperlukan Kode Akses",
|
||||||
Tips: "Masukkan kode akses di bawah",
|
Tips: "Masukkan kode akses di bawah",
|
||||||
|
SubTips: "Atau masukkan kunci API OpenAI Anda",
|
||||||
Input: "Kode Akses",
|
Input: "Kode Akses",
|
||||||
Confirm: "Konfirmasi",
|
Confirm: "Konfirmasi",
|
||||||
Later: "Nanti",
|
Later: "Nanti",
|
||||||
@@ -60,7 +60,9 @@ const id: PartialLocaleType = {
|
|||||||
if (submitKey === String(SubmitKey.Enter)) {
|
if (submitKey === String(SubmitKey.Enter)) {
|
||||||
inputHints += ", Shift + Enter untuk membalut";
|
inputHints += ", Shift + Enter untuk membalut";
|
||||||
}
|
}
|
||||||
return inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah";
|
return (
|
||||||
|
inputHints + ", / untuk mencari prompt, : untuk menggunakan perintah"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
Send: "Kirim",
|
Send: "Kirim",
|
||||||
Config: {
|
Config: {
|
||||||
@@ -117,33 +119,35 @@ const id: PartialLocaleType = {
|
|||||||
Title: "Setel Ulang Semua Pengaturan",
|
Title: "Setel Ulang Semua Pengaturan",
|
||||||
SubTitle: "Mengembalikan semua pengaturan ke nilai default",
|
SubTitle: "Mengembalikan semua pengaturan ke nilai default",
|
||||||
Action: "Setel Ulang",
|
Action: "Setel Ulang",
|
||||||
Confirm: "Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
|
Confirm:
|
||||||
|
"Anda yakin ingin mengembalikan semua pengaturan ke nilai default?",
|
||||||
},
|
},
|
||||||
Clear: {
|
Clear: {
|
||||||
Title: "Hapus Semua Data",
|
Title: "Hapus Semua Data",
|
||||||
SubTitle: "Menghapus semua pesan dan pengaturan",
|
SubTitle: "Semua data yang tersimpan secara lokal akan dihapus",
|
||||||
Action: "Hapus",
|
Action: "Hapus",
|
||||||
Confirm: "Anda yakin ingin menghapus semua pesan dan pengaturan?",
|
Confirm:
|
||||||
|
"Apakah Anda yakin ingin menghapus semua data yang tersimpan secara lokal?",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Lang: {
|
Lang: {
|
||||||
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
Name: "Bahasa", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||||
All: "Semua Bahasa",
|
All: "Semua Bahasa",
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
FontSize: {
|
FontSize: {
|
||||||
Title: "Ukuran Font",
|
Title: "Ukuran Font",
|
||||||
SubTitle: "Ubah ukuran font konten chat",
|
SubTitle: "Ubah ukuran font konten chat",
|
||||||
},
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "Suntikkan Petunjuk Sistem",
|
Title: "Suntikkan Petunjuk Sistem",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
|
"Tambahkan petunjuk simulasi sistem ChatGPT di awal daftar pesan yang diminta dalam setiap permintaan",
|
||||||
},
|
},
|
||||||
InputTemplate: {
|
InputTemplate: {
|
||||||
Title: "Template Input",
|
Title: "Template Input",
|
||||||
SubTitle: "Pesan baru akan diisi menggunakan template ini",
|
SubTitle: "Pesan baru akan diisi menggunakan template ini",
|
||||||
},
|
},
|
||||||
|
|
||||||
Update: {
|
Update: {
|
||||||
Version: (x: string) => `Version: ${x}`,
|
Version: (x: string) => `Version: ${x}`,
|
||||||
@@ -154,9 +158,40 @@ const id: PartialLocaleType = {
|
|||||||
GoToUpdate: "Perbarui Sekarang",
|
GoToUpdate: "Perbarui Sekarang",
|
||||||
},
|
},
|
||||||
AutoGenerateTitle: {
|
AutoGenerateTitle: {
|
||||||
Title: "Hasilkan Judul Otomatis",
|
Title: "Hasilkan Judul Otomatis",
|
||||||
SubTitle: "Hasilkan judul yang sesuai berdasarkan konten percakapan",
|
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",
|
SendKey: "Kirim",
|
||||||
Theme: "Tema",
|
Theme: "Tema",
|
||||||
TightBorder: "Batas Ketat",
|
TightBorder: "Batas Ketat",
|
||||||
@@ -176,76 +211,77 @@ const id: PartialLocaleType = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "Nonaktifkan Otomatisasi",
|
Title: "Nonaktifkan Otomatisasi",
|
||||||
SubTitle: "Aktifkan/Matikan 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",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
HistoryCount: {
|
List: "Daftar Prompt",
|
||||||
Title: "Jumlah Pesan Riwayat",
|
ListCount: (builtin: number, custom: number) =>
|
||||||
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
|
`${builtin} bawaan, ${custom} penggunaan khusus`,
|
||||||
|
Edit: "Edit",
|
||||||
|
Modal: {
|
||||||
|
Title: "Daftar Prompt",
|
||||||
|
Add: "Tambahkan",
|
||||||
|
Search: "Cari Prompt",
|
||||||
},
|
},
|
||||||
CompressThreshold: {
|
EditModal: {
|
||||||
Title: "Batas Kompresi Riwayat",
|
Title: "Edit Prompt",
|
||||||
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",
|
HistoryCount: {
|
||||||
SubTitle(used: any, total: any) {
|
Title: "Jumlah Pesan Riwayat",
|
||||||
return `Digunakan bulan ini: ${used}, total langganan: ${total}`;
|
SubTitle: "Jumlah pesan yang akan dikirim setiap permintaan",
|
||||||
},
|
},
|
||||||
IsChecking: "Memeriksa...",
|
CompressThreshold: {
|
||||||
Check: "Periksa",
|
Title: "Batas Kompresi Riwayat",
|
||||||
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
|
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: {
|
IsChecking: "Memeriksa...",
|
||||||
Title: "Kode Akses",
|
Check: "Periksa",
|
||||||
SubTitle: "Kontrol akses diaktifkan",
|
NoAccess: "Masukkan kunci API untuk memeriksa saldo",
|
||||||
Placeholder: "Diperlukan kode akses",
|
},
|
||||||
},
|
AccessCode: {
|
||||||
Endpoint: {
|
Title: "Kode Akses",
|
||||||
Title: "Endpoint",
|
SubTitle: "Kontrol akses diaktifkan",
|
||||||
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
|
Placeholder: "Diperlukan kode akses",
|
||||||
},
|
},
|
||||||
Model: "Model",
|
Endpoint: {
|
||||||
Temperature: {
|
Title: "Endpoint",
|
||||||
Title: "Suhu",
|
SubTitle: "Harus dimulai dengan http(s):// untuk endpoint kustom",
|
||||||
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
|
},
|
||||||
},
|
Model: "Model",
|
||||||
TopP: {
|
Temperature: {
|
||||||
Title: "Top P",
|
Title: "Suhu",
|
||||||
SubTitle: "Tidak mengubah nilai dengan suhu",
|
SubTitle: "Semakin tinggi nilainya, semakin acak keluarannya",
|
||||||
},
|
},
|
||||||
MaxTokens: {
|
TopP: {
|
||||||
Title: "Token Maksimum",
|
Title: "Top P",
|
||||||
SubTitle: "Panjang maksimum token input dan output",
|
SubTitle: "Tidak mengubah nilai dengan suhu",
|
||||||
},
|
},
|
||||||
PresencePenalty: {
|
MaxTokens: {
|
||||||
Title: "Penalti Kehadiran",
|
Title: "Token Maksimum",
|
||||||
SubTitle: "Semakin tinggi nilai, semakin mungkin topik baru muncul",
|
SubTitle: "Panjang maksimum token input dan output",
|
||||||
},
|
},
|
||||||
FrequencyPenalty: {
|
PresencePenalty: {
|
||||||
Title: "Penalti Frekuensi",
|
Title: "Penalti Kehadiran",
|
||||||
SubTitle: "Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
|
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: {
|
Store: {
|
||||||
DefaultTopic: "Percakapan Baru",
|
DefaultTopic: "Percakapan Baru",
|
||||||
@@ -261,8 +297,13 @@ const id: PartialLocaleType = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Copy: {
|
Copy: {
|
||||||
Success: "Berhasil disalin ke clipboard",
|
Success: "Tersalin ke clipboard",
|
||||||
Failed: "Gagal menyalin, berikan izin untuk memberikan izin",
|
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: {
|
Context: {
|
||||||
Toast: (x: any) => `Dengan ${x} promp kontekstual`,
|
Toast: (x: any) => `Dengan ${x} promp kontekstual`,
|
||||||
@@ -341,7 +382,7 @@ const id: PartialLocaleType = {
|
|||||||
Model: "Model",
|
Model: "Model",
|
||||||
Messages: "Pesan",
|
Messages: "Pesan",
|
||||||
Topic: "Topik",
|
Topic: "Topik",
|
||||||
Time: "Waktu",
|
Time: "Tanggal & Waktu",
|
||||||
},
|
},
|
||||||
URLCommand: {
|
URLCommand: {
|
||||||
Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ",
|
Code: "Kode akses terdeteksi dari url, konfirmasi untuk mendaftar ? ",
|
||||||
|
@@ -1,6 +1,12 @@
|
|||||||
import { LLMModel } from "../client/api";
|
import { LLMModel } from "../client/api";
|
||||||
|
import { isMacOS } from "../utils";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, StoreKey } from "../constant";
|
import {
|
||||||
|
DEFAULT_INPUT_TEMPLATE,
|
||||||
|
DEFAULT_MODELS,
|
||||||
|
DEFAULT_SIDEBAR_WIDTH,
|
||||||
|
StoreKey,
|
||||||
|
} from "../constant";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
|
|
||||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||||
@@ -22,14 +28,14 @@ export enum Theme {
|
|||||||
export const DEFAULT_CONFIG = {
|
export const DEFAULT_CONFIG = {
|
||||||
lastUpdate: Date.now(), // timestamp, to merge state
|
lastUpdate: Date.now(), // timestamp, to merge state
|
||||||
|
|
||||||
submitKey: SubmitKey.CtrlEnter as SubmitKey,
|
submitKey: isMacOS() ? SubmitKey.MetaEnter : SubmitKey.CtrlEnter,
|
||||||
avatar: "1f603",
|
avatar: "1f603",
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
theme: Theme.Auto as Theme,
|
theme: Theme.Auto as Theme,
|
||||||
tightBorder: !!getClientConfig()?.isApp,
|
tightBorder: !!getClientConfig()?.isApp,
|
||||||
sendPreviewBubble: true,
|
sendPreviewBubble: true,
|
||||||
enableAutoGenerateTitle: true,
|
enableAutoGenerateTitle: true,
|
||||||
sidebarWidth: 300,
|
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||||
|
|
||||||
disablePromptHint: false,
|
disablePromptHint: false,
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
|
import { getClientConfig } from "../config/client";
|
||||||
import { Updater } from "../typing";
|
import { Updater } from "../typing";
|
||||||
import { ApiPath, StoreKey } from "../constant";
|
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
@@ -20,29 +21,32 @@ export interface WebDavConfig {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
export type SyncStore = GetStoreState<typeof useSyncStore>;
|
export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||||
|
|
||||||
export const useSyncStore = createPersistStore(
|
const DEFAULT_SYNC_STATE = {
|
||||||
{
|
provider: ProviderType.WebDAV,
|
||||||
provider: ProviderType.WebDAV,
|
useProxy: true,
|
||||||
useProxy: true,
|
proxyUrl: corsPath(ApiPath.Cors),
|
||||||
proxyUrl: corsPath(ApiPath.Cors),
|
|
||||||
|
|
||||||
webdav: {
|
webdav: {
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
|
||||||
|
|
||||||
upstash: {
|
|
||||||
endpoint: "",
|
|
||||||
username: "",
|
|
||||||
apiKey: "",
|
|
||||||
},
|
|
||||||
|
|
||||||
lastSyncTime: 0,
|
|
||||||
lastProvider: "",
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
upstash: {
|
||||||
|
endpoint: "",
|
||||||
|
username: STORAGE_KEY,
|
||||||
|
apiKey: "",
|
||||||
|
},
|
||||||
|
|
||||||
|
lastSyncTime: 0,
|
||||||
|
lastProvider: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSyncStore = createPersistStore(
|
||||||
|
DEFAULT_SYNC_STATE,
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
coundSync() {
|
coundSync() {
|
||||||
const config = get()[get().provider];
|
const config = get()[get().provider];
|
||||||
@@ -55,7 +59,11 @@ export const useSyncStore = createPersistStore(
|
|||||||
|
|
||||||
export() {
|
export() {
|
||||||
const state = getLocalAppState();
|
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);
|
downloadAs(JSON.stringify(state), fileName);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -108,6 +116,16 @@ export const useSyncStore = createPersistStore(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: StoreKey.Sync,
|
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;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@@ -2,8 +2,11 @@ import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
|
|||||||
import { api } from "../client/api";
|
import { api } from "../client/api";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
|
import ChatGptIcon from "../icons/chatgpt.png";
|
||||||
|
import Locale from "../locales";
|
||||||
|
|
||||||
const ONE_MINUTE = 60 * 1000;
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
const isApp = !!getClientConfig()?.isApp;
|
||||||
|
|
||||||
function formatVersionDate(t: string) {
|
function formatVersionDate(t: string) {
|
||||||
const d = new Date(+t);
|
const d = new Date(+t);
|
||||||
@@ -80,6 +83,38 @@ export const useUpdateStore = createPersistStore(
|
|||||||
set(() => ({
|
set(() => ({
|
||||||
remoteVersion: remoteId,
|
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);
|
console.log("[Got Upstream] ", remoteId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Fetch Upstream Commit Id]", error);
|
console.error("[Fetch Upstream Commit Id]", error);
|
||||||
|
55
app/utils.ts
55
app/utils.ts
@@ -31,12 +31,41 @@ export async function copyToClipboard(text: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadAs(text: string, filename: string) {
|
export async function downloadAs(text: string, filename: string) {
|
||||||
const element = document.createElement("a");
|
if (window.__TAURI__) {
|
||||||
element.setAttribute(
|
const result = await window.__TAURI__.dialog.save({
|
||||||
"href",
|
defaultPath: `${filename}`,
|
||||||
"data:text/plain;charset=utf-8," + encodeURIComponent(text),
|
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.setAttribute("download", filename);
|
||||||
|
|
||||||
element.style.display = "none";
|
element.style.display = "none";
|
||||||
@@ -46,7 +75,7 @@ export function downloadAs(text: string, filename: string) {
|
|||||||
|
|
||||||
document.body.removeChild(element);
|
document.body.removeChild(element);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
export function readFromFile() {
|
export function readFromFile() {
|
||||||
return new Promise<string>((res, rej) => {
|
return new Promise<string>((res, rej) => {
|
||||||
const fileInput = document.createElement("input");
|
const fileInput = document.createElement("input");
|
||||||
@@ -173,3 +202,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
|
|||||||
export function getCSSVar(varName: string) {
|
export function getCSSVar(varName: string) {
|
||||||
return getComputedStyle(document.body).getPropertyValue(varName).trim();
|
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
|
||||||
|
}
|
||||||
|
@@ -1,25 +1,87 @@
|
|||||||
|
import { STORAGE_KEY } from "@/app/constant";
|
||||||
import { SyncStore } from "@/app/store/sync";
|
import { SyncStore } from "@/app/store/sync";
|
||||||
|
import { corsFetch } from "../cors";
|
||||||
|
import { chunks } from "../format";
|
||||||
|
|
||||||
export type UpstashConfig = SyncStore["upstash"];
|
export type UpstashConfig = SyncStore["upstash"];
|
||||||
export type UpStashClient = ReturnType<typeof createUpstashClient>;
|
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 {
|
return {
|
||||||
async check() {
|
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() {
|
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() {
|
async set(_: string, value: string) {
|
||||||
throw Error("[Sync] not implemented");
|
// 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() {
|
headers() {
|
||||||
return {
|
return {
|
||||||
Authorization: `Basic ${config.apiKey}`,
|
Authorization: `Bearer ${config.apiKey}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
path(path: string) {
|
path(path: string) {
|
||||||
|
@@ -20,10 +20,8 @@ export function createWebDavClient(store: SyncStore) {
|
|||||||
headers: this.headers(),
|
headers: this.headers(),
|
||||||
proxyUrl,
|
proxyUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[WebDav] check", res.status, res.statusText);
|
console.log("[WebDav] check", res.status, res.statusText);
|
||||||
|
return [201, 200, 404, 301, 302, 307, 308].includes(res.status);
|
||||||
return [201, 200, 404].includes(res.status);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[WebDav] failed to check", e);
|
console.error("[WebDav] failed to check", e);
|
||||||
}
|
}
|
||||||
|
@@ -11,3 +11,18 @@ export function prettyObject(msg: any) {
|
|||||||
}
|
}
|
||||||
return ["```json", msg, "```"].join("\n");
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -69,6 +69,9 @@ const MergeStates: StateMerger = {
|
|||||||
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
||||||
|
|
||||||
remoteState.sessions.forEach((remoteSession) => {
|
remoteState.sessions.forEach((remoteSession) => {
|
||||||
|
// skip empty chats
|
||||||
|
if (remoteSession.messages.length === 0) return;
|
||||||
|
|
||||||
const localSession = localSessions[remoteSession.id];
|
const localSession = localSessions[remoteSession.id];
|
||||||
if (!localSession) {
|
if (!localSession) {
|
||||||
// if remote session is new, just merge it
|
// if remote session is new, just merge it
|
||||||
|
@@ -17,7 +17,7 @@ tauri-build = { version = "1.3.0", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
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" }
|
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "ChatGPT Next Web",
|
"productName": "ChatGPT Next Web",
|
||||||
"version": "2.9.6"
|
"version": "2.9.8"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
@@ -44,6 +44,12 @@
|
|||||||
"startDragging": true,
|
"startDragging": true,
|
||||||
"unmaximize": true,
|
"unmaximize": true,
|
||||||
"unminimize": true
|
"unminimize": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"all": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
Reference in New Issue
Block a user