Merge branch 'Yidadaa:main' into main
|
@ -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)
|
||||||
|
|
|
@ -159,7 +159,7 @@ Your openai api key.
|
||||||
|
|
||||||
### `CODE` (optional)
|
### `CODE` (optional)
|
||||||
|
|
||||||
Access passsword, separated by comma.
|
Access password, separated by comma.
|
||||||
|
|
||||||
### `BASE_URL` (optional)
|
### `BASE_URL` (optional)
|
||||||
|
|
||||||
|
@ -257,6 +257,10 @@ If your proxy needs password, use:
|
||||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Synchronizing Chat Records (UpStash)
|
||||||
|
|
||||||
|
| [简体中文](./docs/synchronise-chat-logs-cn.md) | [English](./docs/synchronise-chat-logs-en.md) | [Italiano](./docs/synchronise-chat-logs-es.md) | [日本語](./docs/synchronise-chat-logs-ja.md) | [한국어](./docs/synchronise-chat-logs-ko.md)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
> Please go to the [docs][./docs] directory for more documentation instructions.
|
> Please go to the [docs][./docs] directory for more documentation instructions.
|
||||||
|
@ -309,6 +313,7 @@ If you want to add a new translation, read this [document](./docs/translation.md
|
||||||
[@AnsonHyq](https://github.com/AnsonHyq)
|
[@AnsonHyq](https://github.com/AnsonHyq)
|
||||||
[@synwith](https://github.com/synwith)
|
[@synwith](https://github.com/synwith)
|
||||||
[@piksonGit](https://github.com/piksonGit)
|
[@piksonGit](https://github.com/piksonGit)
|
||||||
|
[@ouyangzhiping](https://github.com/ouyangzhiping)
|
||||||
|
|
||||||
### Contributor
|
### Contributor
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
### 本地开发
|
### 本地开发
|
||||||
|
@ -169,6 +169,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||||
|
|
||||||
⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。
|
⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。
|
||||||
|
|
||||||
|
|
||||||
## 鸣谢
|
## 鸣谢
|
||||||
|
|
||||||
### 捐赠者
|
### 捐赠者
|
||||||
|
@ -185,3 +186,4 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||||
## 开源协议
|
## 开源协议
|
||||||
|
|
||||||
[MIT](https://opensource.org/license/mit/)
|
[MIT](https://opensource.org/license/mit/)
|
||||||
|
|
||||||
|
|
|
@ -169,6 +169,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||||
|
|
||||||
⚠️ 주의: 설치 중 문제가 발생한 경우, docker로 배포하세요.
|
⚠️ 주의: 설치 중 문제가 발생한 경우, docker로 배포하세요.
|
||||||
|
|
||||||
|
|
||||||
## 감사의 말
|
## 감사의 말
|
||||||
|
|
||||||
### 기부자
|
### 기부자
|
||||||
|
@ -185,3 +186,4 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||||
## 오픈소스 라이센스
|
## 오픈소스 라이센스
|
||||||
|
|
||||||
[MIT](https://opensource.org/license/mit/)
|
[MIT](https://opensource.org/license/mit/)
|
||||||
|
|
||||||
|
|
|
@ -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={() => {
|
<IconButton
|
||||||
|
text={Locale.Auth.Later}
|
||||||
|
onClick={() => {
|
||||||
resetAccessCode();
|
resetAccessCode();
|
||||||
goHome();
|
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;
|
||||||
|
@ -1154,9 +1154,15 @@ function _Chat() {
|
||||||
</div>
|
</div>
|
||||||
{isUser ? (
|
{isUser ? (
|
||||||
<Avatar avatar={config.avatar} />
|
<Avatar avatar={config.avatar} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{["system"].includes(message.role) ? (
|
||||||
|
<Avatar avatar="2699-fe0f" />
|
||||||
) : (
|
) : (
|
||||||
<MaskAvatar mask={session.mask} />
|
<MaskAvatar mask={session.mask} />
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
|
|
|
@ -433,16 +433,45 @@ 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;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blob = await toPng(dom);
|
||||||
if (!blob) return;
|
if (!blob) return;
|
||||||
|
|
||||||
if (isMobile || getClientConfig()?.isApp) {
|
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 {
|
||||||
showImageModal(blob);
|
showImageModal(blob);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.download = `${props.topic}.png`;
|
link.download = `${props.topic}.png`;
|
||||||
|
@ -450,8 +479,9 @@ export function ImagePreviewer(props: {
|
||||||
link.click();
|
link.click();
|
||||||
refreshPreview();
|
refreshPreview();
|
||||||
}
|
}
|
||||||
})
|
} catch (error) {
|
||||||
.catch((e) => console.log("[Export Image] ", e));
|
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,7 @@ 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 = getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAsyncGoogleFont();
|
loadAsyncGoogleFont();
|
||||||
|
@ -134,11 +138,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 />
|
||||||
|
|
|
@ -393,11 +393,13 @@ export function MaskPage() {
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const masks = searchText.length > 0 ? searchMasks : allMasks;
|
const masks = searchText.length > 0 ? searchMasks : allMasks;
|
||||||
|
|
||||||
// simple search, will refactor later
|
// refactored already, now it accurate
|
||||||
const onSearch = (text: string) => {
|
const onSearch = (text: string) => {
|
||||||
setSearchText(text);
|
setSearchText(text);
|
||||||
if (text.length > 0) {
|
if (text.length > 0) {
|
||||||
const result = allMasks.filter((m) => m.name.includes(text));
|
const result = allMasks.filter((m) =>
|
||||||
|
m.name.toLowerCase().includes(text.toLowerCase())
|
||||||
|
);
|
||||||
setSearchMasks(result);
|
setSearchMasks(result);
|
||||||
} else {
|
} else {
|
||||||
setSearchMasks(allMasks);
|
setSearchMasks(allMasks);
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -718,7 +753,7 @@ export function Settings() {
|
||||||
title={`${config.fontSize ?? 14}px`}
|
title={`${config.fontSize ?? 14}px`}
|
||||||
value={config.fontSize}
|
value={config.fontSize}
|
||||||
min="12"
|
min="12"
|
||||||
max="18"
|
max="40"
|
||||||
step="1"
|
step="1"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig(
|
updateConfig(
|
||||||
|
|
|
@ -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) => {
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
lastUpdateTime.current = Date.now();
|
lastUpdateTime.current = Date.now();
|
||||||
const d = e.clientX - startX.current;
|
const d = e.clientX - startX.current;
|
||||||
const nextWidth = limit(startDragWidth.current + d);
|
const nextWidth = limit(startDragWidth.current + d);
|
||||||
config.update((config) => (config.sidebarWidth = nextWidth));
|
config.update((config) => {
|
||||||
|
if (nextWidth < MIN_SIDEBAR_WIDTH) {
|
||||||
|
config.sidebarWidth = NARROW_SIDEBAR_WIDTH;
|
||||||
|
} else {
|
||||||
|
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 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://ab.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;
|
||||||
|
|
|
@ -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,13 +119,15 @@ 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: {
|
||||||
|
@ -157,6 +161,37 @@ const id: PartialLocaleType = {
|
||||||
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",
|
||||||
|
@ -244,7 +279,8 @@ const id: PartialLocaleType = {
|
||||||
},
|
},
|
||||||
FrequencyPenalty: {
|
FrequencyPenalty: {
|
||||||
Title: "Penalti Frekuensi",
|
Title: "Penalti Frekuensi",
|
||||||
SubTitle: "Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
|
SubTitle:
|
||||||
|
"Semakin tinggi nilai, semakin rendah kemungkinan penggunaan ulang baris yang sama",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Store: {
|
Store: {
|
||||||
|
@ -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 ? ",
|
||||||
|
|
|
@ -7,13 +7,13 @@ const tw: PartialLocaleType = {
|
||||||
Unauthorized: "目前您的狀態是未授權,請前往[設定頁面](/#/auth)輸入授權碼。",
|
Unauthorized: "目前您的狀態是未授權,請前往[設定頁面](/#/auth)輸入授權碼。",
|
||||||
},
|
},
|
||||||
ChatItem: {
|
ChatItem: {
|
||||||
ChatItemCount: (count: number) => `${count} 條對話`,
|
ChatItemCount: (count: number) => `${count} 則對話`,
|
||||||
},
|
},
|
||||||
Chat: {
|
Chat: {
|
||||||
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`,
|
SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 則對話`,
|
||||||
Actions: {
|
Actions: {
|
||||||
ChatList: "查看訊息列表",
|
ChatList: "檢視訊息列表",
|
||||||
CompressedHistory: "查看壓縮後的歷史 Prompt",
|
CompressedHistory: "檢視壓縮後的歷史 Prompt",
|
||||||
Export: "匯出聊天紀錄",
|
Export: "匯出聊天紀錄",
|
||||||
Copy: "複製",
|
Copy: "複製",
|
||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
|
@ -23,15 +23,15 @@ const tw: PartialLocaleType = {
|
||||||
Rename: "重新命名對話",
|
Rename: "重新命名對話",
|
||||||
Typing: "正在輸入…",
|
Typing: "正在輸入…",
|
||||||
Input: (submitKey: string) => {
|
Input: (submitKey: string) => {
|
||||||
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`;
|
var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可傳送`;
|
||||||
if (submitKey === String(SubmitKey.Enter)) {
|
if (submitKey === String(SubmitKey.Enter)) {
|
||||||
inputHints += ",Shift + Enter 鍵換行";
|
inputHints += ",Shift + Enter 鍵換行";
|
||||||
}
|
}
|
||||||
return inputHints;
|
return inputHints;
|
||||||
},
|
},
|
||||||
Send: "發送",
|
Send: "傳送",
|
||||||
Config: {
|
Config: {
|
||||||
Reset: "重置預設",
|
Reset: "重設",
|
||||||
SaveAs: "另存新檔",
|
SaveAs: "另存新檔",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -46,7 +46,7 @@ const tw: PartialLocaleType = {
|
||||||
Title: "上下文記憶 Prompt",
|
Title: "上下文記憶 Prompt",
|
||||||
EmptyContent: "尚未記憶",
|
EmptyContent: "尚未記憶",
|
||||||
Copy: "複製全部",
|
Copy: "複製全部",
|
||||||
Send: "發送記憶",
|
Send: "傳送記憶",
|
||||||
Reset: "重設對話",
|
Reset: "重設對話",
|
||||||
ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?",
|
ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?",
|
||||||
},
|
},
|
||||||
|
@ -71,22 +71,22 @@ const tw: PartialLocaleType = {
|
||||||
},
|
},
|
||||||
InjectSystemPrompts: {
|
InjectSystemPrompts: {
|
||||||
Title: "匯入系統提示",
|
Title: "匯入系統提示",
|
||||||
SubTitle: "強制在每個請求的訊息列表開頭添加一個模擬 ChatGPT 的系統提示",
|
SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
|
||||||
},
|
},
|
||||||
Update: {
|
Update: {
|
||||||
Version: (x: string) => `當前版本:${x}`,
|
Version: (x: string) => `目前版本:${x}`,
|
||||||
IsLatest: "已是最新版本",
|
IsLatest: "已是最新版本",
|
||||||
CheckUpdate: "檢查更新",
|
CheckUpdate: "檢查更新",
|
||||||
IsChecking: "正在檢查更新...",
|
IsChecking: "正在檢查更新...",
|
||||||
FoundUpdate: (x: string) => `發現新版本:${x}`,
|
FoundUpdate: (x: string) => `發現新版本:${x}`,
|
||||||
GoToUpdate: "前往更新",
|
GoToUpdate: "前往更新",
|
||||||
},
|
},
|
||||||
SendKey: "發送鍵",
|
SendKey: "傳送鍵",
|
||||||
Theme: "主題",
|
Theme: "主題",
|
||||||
TightBorder: "緊湊邊框",
|
TightBorder: "緊湊邊框",
|
||||||
SendPreviewBubble: {
|
SendPreviewBubble: {
|
||||||
Title: "預覽氣泡",
|
Title: "預覽氣泡",
|
||||||
SubTitle: "在預覽氣泡中預覽 Markdown 内容",
|
SubTitle: "在預覽氣泡中預覽 Markdown 內容",
|
||||||
},
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Splash: {
|
Splash: {
|
||||||
|
@ -101,7 +101,7 @@ const tw: PartialLocaleType = {
|
||||||
},
|
},
|
||||||
List: "自定義提示詞列表",
|
List: "自定義提示詞列表",
|
||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`內建 ${builtin} 條,用戶定義 ${custom} 條`,
|
`內建 ${builtin} 條,使用者定義 ${custom} 條`,
|
||||||
Edit: "編輯",
|
Edit: "編輯",
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "提示詞列表",
|
Title: "提示詞列表",
|
||||||
|
@ -132,7 +132,7 @@ const tw: PartialLocaleType = {
|
||||||
},
|
},
|
||||||
IsChecking: "正在檢查…",
|
IsChecking: "正在檢查…",
|
||||||
Check: "重新檢查",
|
Check: "重新檢查",
|
||||||
NoAccess: "輸入API Key查看餘額",
|
NoAccess: "輸入 API Key 檢視餘額",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
AccessCode: {
|
||||||
Title: "授權碼",
|
Title: "授權碼",
|
||||||
|
@ -150,7 +150,7 @@ const tw: PartialLocaleType = {
|
||||||
},
|
},
|
||||||
PresencePenalty: {
|
PresencePenalty: {
|
||||||
Title: "話題新穎度 (presence_penalty)",
|
Title: "話題新穎度 (presence_penalty)",
|
||||||
SubTitle: "值越大,越有可能擴展到新話題",
|
SubTitle: "值越大,越有可能拓展到新話題",
|
||||||
},
|
},
|
||||||
FrequencyPenalty: {
|
FrequencyPenalty: {
|
||||||
Title: "頻率懲罰度 (frequency_penalty)",
|
Title: "頻率懲罰度 (frequency_penalty)",
|
||||||
|
@ -163,7 +163,7 @@ const tw: PartialLocaleType = {
|
||||||
Error: "出錯了,請稍後再嘗試",
|
Error: "出錯了,請稍後再嘗試",
|
||||||
Prompt: {
|
Prompt: {
|
||||||
History: (content: string) =>
|
History: (content: string) =>
|
||||||
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
"這是 AI 與使用者的歷史聊天總結,作為前情提要:" + content,
|
||||||
Topic:
|
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.",
|
"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:
|
Summarize:
|
||||||
|
@ -192,16 +192,16 @@ const tw: PartialLocaleType = {
|
||||||
Item: {
|
Item: {
|
||||||
Info: (count: number) => `包含 ${count} 條預設對話`,
|
Info: (count: number) => `包含 ${count} 條預設對話`,
|
||||||
Chat: "對話",
|
Chat: "對話",
|
||||||
View: "查看",
|
View: "檢視",
|
||||||
Edit: "編輯",
|
Edit: "編輯",
|
||||||
Delete: "删除",
|
Delete: "刪除",
|
||||||
DeleteConfirm: "確認删除?",
|
DeleteConfirm: "確認刪除?",
|
||||||
},
|
},
|
||||||
EditModal: {
|
EditModal: {
|
||||||
Title: (readonly: boolean) =>
|
Title: (readonly: boolean) =>
|
||||||
`編輯預設面具 ${readonly ? "(只读)" : ""}`,
|
`編輯預設面具 ${readonly ? "(只讀)" : ""}`,
|
||||||
Download: "下載預設",
|
Download: "下載預設",
|
||||||
Clone: "克隆預設",
|
Clone: "複製預設",
|
||||||
},
|
},
|
||||||
Config: {
|
Config: {
|
||||||
Avatar: "角色頭像",
|
Avatar: "角色頭像",
|
||||||
|
@ -215,7 +215,7 @@ const tw: PartialLocaleType = {
|
||||||
SubTitle: "現在開始,與面具背後的靈魂思維碰撞",
|
SubTitle: "現在開始,與面具背後的靈魂思維碰撞",
|
||||||
More: "搜尋更多",
|
More: "搜尋更多",
|
||||||
NotShow: "不再呈現",
|
NotShow: "不再呈現",
|
||||||
ConfirmNoShow: "確認禁用?禁用後可以随時在設定中重新啟用。",
|
ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。",
|
||||||
},
|
},
|
||||||
UI: {
|
UI: {
|
||||||
Confirm: "確認",
|
Confirm: "確認",
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const EN_MASKS: BuiltinMask[] = [
|
||||||
id: "prompt-improve-0",
|
id: "prompt-improve-0",
|
||||||
role: "user",
|
role: "user",
|
||||||
content:
|
content:
|
||||||
'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finsihed and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.',
|
'Read all of the instructions below and once you understand them say "Shall we begin:"\n \nI want you to become my Prompt Creator. Your goal is to help me craft the best possible prompt for my needs. The prompt will be used by you, ChatGPT. You will follow the following process:\nYour first response will be to ask me what the prompt should be about. I will provide my answer, but we will need to improve it through continual iterations by going through the next steps.\n \nBased on my input, you will generate 3 sections.\n \nRevised Prompt (provide your rewritten prompt. it should be clear, concise, and easily understood by you)\nSuggestions (provide 3 suggestions on what details to include in the prompt to improve it)\nQuestions (ask the 3 most relevant questions pertaining to what additional information is needed from me to improve the prompt)\n \nAt the end of these sections give me a reminder of my options which are:\n \nOption 1: Read the output and provide more info or answer one or more of the questions\nOption 2: Type "Use this prompt" and I will submit this as a query for you\nOption 3: Type "Restart" to restart this process from the beginning\nOption 4: Type "Quit" to end this script and go back to a regular ChatGPT session\n \nIf I type "Option 2", "2" or "Use this prompt" then we have finished and you should use the Revised Prompt as a prompt to generate my request\nIf I type "option 3", "3" or "Restart" then forget the latest Revised Prompt and restart this process\nIf I type "Option 4", "4" or "Quit" then finish this process and revert back to your general mode of operation\n\n\nWe will continue this iterative process with me providing additional information to you and you updating the prompt in the Revised Prompt section until it is complete.',
|
||||||
date: "",
|
date: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import { create } from "zustand";
|
|
||||||
import { persist } from "zustand/middleware";
|
|
||||||
|
|
||||||
import { trimTopic } from "../utils";
|
import { trimTopic } from "../utils";
|
||||||
|
|
||||||
import Locale, { getLang } from "../locales";
|
import Locale, { getLang } from "../locales";
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
|
@ -64,7 +70,7 @@ export function limitNumber(
|
||||||
max: number,
|
max: number,
|
||||||
defaultValue: number,
|
defaultValue: number,
|
||||||
) {
|
) {
|
||||||
if (typeof x !== "number" || isNaN(x)) {
|
if (isNaN(x)) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,9 +133,7 @@ export const useAppConfig = createPersistStore(
|
||||||
.customModels.split(",")
|
.customModels.split(",")
|
||||||
.filter((v) => !!v && v.length > 0)
|
.filter((v) => !!v && v.length > 0)
|
||||||
.map((m) => ({ name: m, available: true }));
|
.map((m) => ({ name: m, available: true }));
|
||||||
|
return get().models.concat(customModels);
|
||||||
const models = get().models.concat(customModels);
|
|
||||||
return models;
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,10 +21,10 @@ 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),
|
||||||
|
@ -36,13 +37,16 @@ export const useSyncStore = createPersistStore(
|
||||||
|
|
||||||
upstash: {
|
upstash: {
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
username: "",
|
username: STORAGE_KEY,
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
lastSyncTime: 0,
|
lastSyncTime: 0,
|
||||||
lastProvider: "",
|
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -93,7 +101,7 @@ export const useSyncStore = createPersistStore(
|
||||||
mergeAppState(localState, remoteState);
|
mergeAppState(localState, remoteState);
|
||||||
setLocalAppState(localState);
|
setLocalAppState(localState);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("[Sync] failed to get remoate state", e);
|
console.log("[Sync] failed to get remote state", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.set(config.username, JSON.stringify(localState));
|
await client.set(config.username, JSON.stringify(localState));
|
||||||
|
@ -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);
|
||||||
|
|
45
app/utils.ts
|
@ -31,7 +31,36 @@ export async function copyToClipboard(text: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadAs(text: string, filename: string) {
|
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");
|
const element = document.createElement("a");
|
||||||
element.setAttribute(
|
element.setAttribute(
|
||||||
"href",
|
"href",
|
||||||
|
@ -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
|
||||||
|
|
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 84 KiB |
|
@ -0,0 +1,31 @@
|
||||||
|
# 同步聊天记录
|
||||||
|
## 准备工作
|
||||||
|
- GitHub账号
|
||||||
|
- 拥有自己搭建过的ChatGPT-Next-Web的服务器
|
||||||
|
- [UpStash](https://upstash.com)
|
||||||
|
|
||||||
|
## 开始教程
|
||||||
|
1. 注册UpStash账号
|
||||||
|
2. 创建数据库
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. 找到REST API,分别复制UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN(⚠切记⚠:不要泄露Token!)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. UPSTASH_REDIS_REST_URL和UPSTASH_REDIS_REST_TOKEN复制到你的同步配置,点击**检查可用性**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
如果没什么问题,那就成功了
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Success!
|
||||||
|
|
||||||
|

|
|
@ -0,0 +1,31 @@
|
||||||
|
# Synchronize Chat Logs with UpStash
|
||||||
|
## Prerequisites
|
||||||
|
- GitHub account
|
||||||
|
- Your own ChatGPT-Next-Web server set up
|
||||||
|
- [UpStash](https://upstash.com)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
1. Register for an UpStash account.
|
||||||
|
2. Create a database.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Find the REST API and copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN (⚠Important⚠: Do not share your token!)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Copy UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN into your synchronization configuration, then click **Check Availability**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If everything is in order, you've successfully completed this step.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Success!
|
||||||
|
|
||||||
|

|
|
@ -0,0 +1,31 @@
|
||||||
|
# Sincronizzare i Log delle Chat con UpStash
|
||||||
|
## Prerequisiti
|
||||||
|
- Account GitHub
|
||||||
|
- Server ChatGPT-Next-Web di propria configurazione
|
||||||
|
- [UpStash](https://upstash.com)
|
||||||
|
|
||||||
|
## Per iniziare
|
||||||
|
1. Registrarsi per un account UpStash.
|
||||||
|
2. Creare un database.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Trovare l'API REST e copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN (⚠Importante⚠: Non condividere il token!)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Copiare UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN nella configurazione di sincronizzazione, quindi fare clic su **Verifica la Disponibilità**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Se tutto è in ordine, hai completato con successo questa fase.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Successo!
|
||||||
|
|
||||||
|

|
|
@ -0,0 +1,31 @@
|
||||||
|
# UpStashを使用してチャットログを同期する
|
||||||
|
## 事前準備
|
||||||
|
- GitHubアカウント
|
||||||
|
- 自分自身でChatGPT-Next-Webのサーバーをセットアップしていること
|
||||||
|
- [UpStash](https://upstash.com)
|
||||||
|
|
||||||
|
## 始める
|
||||||
|
1. UpStashアカウントを登録します。
|
||||||
|
2. データベースを作成します。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. REST APIを見つけ、UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENをコピーします(⚠重要⚠:トークンを共有しないでください!)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. UPSTASH_REDIS_REST_URLとUPSTASH_REDIS_REST_TOKENを同期設定にコピーし、次に「可用性を確認」をクリックします。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
すべてが正常であれば、このステップは成功です。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. 成功!
|
||||||
|
|
||||||
|

|
|
@ -0,0 +1,31 @@
|
||||||
|
# UpStash를 사용하여 채팅 기록 동기화
|
||||||
|
## 사전 준비물
|
||||||
|
- GitHub 계정
|
||||||
|
- 자체 ChatGPT-Next-Web 서버 설정
|
||||||
|
- [UpStash](https://upstash.com)
|
||||||
|
|
||||||
|
## 시작하기
|
||||||
|
1. UpStash 계정 등록
|
||||||
|
2. 데이터베이스 생성
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. REST API를 찾아 UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 복사합니다 (⚠주의⚠: 토큰을 공유하지 마십시오!)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. UPSTASH_REDIS_REST_URL 및 UPSTASH_REDIS_REST_TOKEN을 동기화 구성에 복사한 다음 **가용성 확인**을 클릭합니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
모든 것이 정상인 경우,이 단계를 성공적으로 완료했습니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. 성공!
|
||||||
|
|
||||||
|

|
|
@ -1,15 +1,21 @@
|
||||||
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
|
const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache";
|
||||||
|
|
||||||
|
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
|
||||||
|
|
||||||
self.addEventListener("activate", function (event) {
|
self.addEventListener("activate", function (event) {
|
||||||
console.log("ServiceWorker activated.");
|
console.log("ServiceWorker activated.");
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("install", function (event) {
|
workbox.core.clientsClaim();
|
||||||
event.waitUntil(
|
self.addEventListener("message", (event) => {
|
||||||
caches.open(CHATGPT_NEXT_WEB_CACHE).then(function (cache) {
|
if (event.data && event.data.type === "SKIP_WAITING") {
|
||||||
return cache.addAll([]);
|
self.skipWaiting();
|
||||||
}),
|
}
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("fetch", (e) => {});
|
workbox.routing.registerRoute(
|
||||||
|
new RegExp('/*'),
|
||||||
|
new workbox.strategies.StaleWhileRevalidate({
|
||||||
|
cacheName: CHATGPT_NEXT_WEB_CACHE
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
|
@ -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.9"
|
||||||
},
|
},
|
||||||
"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": {
|
||||||
|
|