diff --git a/app/components/auth.tsx b/app/components/auth.tsx
index 9a5b0c65..b82d0e89 100644
--- a/app/components/auth.tsx
+++ b/app/components/auth.tsx
@@ -15,7 +15,8 @@ export function AuthPage() {
const access = useAccessStore();
const goHome = () => navigate(Path.Home);
- const resetAccessCode = () => access.updateCode(""); // Reset access code to empty string
+ const goChat = () => navigate(Path.Chat);
+ const resetAccessCode = () => { access.updateCode(""); access.updateToken(""); }; // Reset access code to empty string
useEffect(() => {
if (getClientConfig()?.isApp) {
@@ -42,17 +43,34 @@ export function AuthPage() {
access.updateCode(e.currentTarget.value);
}}
/>
+ {!access.hideUserApiKey ? (
+ <>
+
{Locale.Auth.SubTips}
+ {
+ access.updateToken(e.currentTarget.value);
+ }}
+ />
+ >
+ ) : null}
+ {
+ resetAccessCode();
+ goHome();
+ }}
/>
- {
- resetAccessCode();
- goHome();
- }} />
);
diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx
index 5b3e8a9a..0a885d87 100644
--- a/app/components/exporter.tsx
+++ b/app/components/exporter.tsx
@@ -433,25 +433,55 @@ export function ImagePreviewer(props: {
const isMobile = useMobileScreen();
- const download = () => {
+ const download = async () => {
showToast(Locale.Export.Image.Toast);
const dom = previewRef.current;
if (!dom) return;
- toPng(dom)
- .then((blob) => {
- if (!blob) return;
-
- if (isMobile || getClientConfig()?.isApp) {
- showImageModal(blob);
+
+ const isApp = getClientConfig()?.isApp;
+
+ try {
+ const blob = await toPng(dom);
+ if (!blob) return;
+
+ if (isMobile || (isApp && window.__TAURI__)) {
+ if (isApp && window.__TAURI__) {
+ const result = await window.__TAURI__.dialog.save({
+ defaultPath: `${props.topic}.png`,
+ filters: [
+ {
+ name: "PNG Files",
+ extensions: ["png"],
+ },
+ {
+ name: "All Files",
+ extensions: ["*"],
+ },
+ ],
+ });
+
+ if (result !== null) {
+ const response = await fetch(blob);
+ const buffer = await response.arrayBuffer();
+ const uint8Array = new Uint8Array(buffer);
+ await window.__TAURI__.fs.writeBinaryFile(result, uint8Array);
+ showToast(Locale.Download.Success);
+ } else {
+ showToast(Locale.Download.Failed);
+ }
} else {
- const link = document.createElement("a");
- link.download = `${props.topic}.png`;
- link.href = blob;
- link.click();
- refreshPreview();
+ showImageModal(blob);
}
- })
- .catch((e) => console.log("[Export Image] ", e));
+ } else {
+ const link = document.createElement("a");
+ link.download = `${props.topic}.png`;
+ link.href = blob;
+ link.click();
+ refreshPreview();
+ }
+ } catch (error) {
+ showToast(Locale.Download.Failed);
+ }
};
const refreshPreview = () => {
diff --git a/app/global.d.ts b/app/global.d.ts
index 524ce77d..e0a2c3f0 100644
--- a/app/global.d.ts
+++ b/app/global.d.ts
@@ -13,5 +13,17 @@ declare module "*.svg";
declare interface Window {
__TAURI__?: {
writeText(text: string): Promise;
+ invoke(command: string, payload?: Record): Promise;
+ dialog: {
+ save(options?: Record): Promise;
+ };
+ fs: {
+ writeBinaryFile(path: string, data: Uint8Array): Promise;
+ };
+ notification:{
+ requestPermission(): Promise;
+ isPermissionGranted(): Promise;
+ sendNotification(options: string | Options): void;
+ };
};
}
diff --git a/app/locales/ar.ts b/app/locales/ar.ts
index 520cb263..d5844acd 100644
--- a/app/locales/ar.ts
+++ b/app/locales/ar.ts
@@ -10,6 +10,7 @@ const ar: PartialLocaleType = {
Auth: {
Title: "تحتاج إلى رمز الوصول",
Tips: "يرجى إدخال رمز الوصول أدناه",
+ SubTips: "أو أدخل مفتاح واجهة برمجة تطبيقات OpenAI الخاص بك",
Input: "رمز الوصول",
Confirm: "تأكيد",
Later: "لاحقًا",
diff --git a/app/locales/bn.ts b/app/locales/bn.ts
index 2d2266b3..2db132ce 100644
--- a/app/locales/bn.ts
+++ b/app/locales/bn.ts
@@ -10,6 +10,7 @@ const bn: PartialLocaleType = {
Auth: {
Title: "একটি অ্যাক্সেস কোড প্রয়োজন",
Tips: "নীচে অ্যাক্সেস কোড ইনপুট করুন",
+ SubTips: "অথবা আপনার OpenAI API কী প্রবেশ করুন",
Input: "অ্যাক্সেস কোড",
Confirm: "নিশ্চিত করুন",
Later: "পরে",
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index b2afc753..4cd963fb 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -13,6 +13,7 @@ const cn = {
Auth: {
Title: "需要密码",
Tips: "管理员开启了密码验证,请在下方填入访问码",
+ SubTips: "或者输入你的 OpenAI API 密钥",
Input: "在此处填写访问码",
Confirm: "确认",
Later: "稍后再说",
@@ -323,6 +324,10 @@ const cn = {
Success: "已写入剪切板",
Failed: "复制失败,请赋予剪切板权限",
},
+ Download: {
+ Success: "内容已下载到您的目录。",
+ Failed: "下载失败。",
+ },
Context: {
Toast: (x: any) => `包含 ${x} 条预设提示词`,
Edit: "当前对话设置",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 697d09d1..928c4b72 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -15,6 +15,7 @@ const en: LocaleType = {
Auth: {
Title: "Need Access Code",
Tips: "Please enter access code below",
+ SubTips: "Or enter your OpenAI API Key",
Input: "access code",
Confirm: "Confirm",
Later: "Later",
@@ -329,6 +330,10 @@ const en: LocaleType = {
Success: "Copied to clipboard",
Failed: "Copy failed, please grant permission to access clipboard",
},
+ Download: {
+ Success: "Content downloaded to your directory.",
+ Failed: "Download failed.",
+ },
Context: {
Toast: (x: any) => `With ${x} contextual prompts`,
Edit: "Current Chat Settings",
diff --git a/app/locales/id.ts b/app/locales/id.ts
index 244c5ade..b5e4a70b 100644
--- a/app/locales/id.ts
+++ b/app/locales/id.ts
@@ -4,12 +4,12 @@ import { PartialLocaleType } from "./index";
const id: PartialLocaleType = {
WIP: "Coming Soon...",
Error: {
- Unauthorized:
- "Akses tidak diizinkan. Silakan [otorisasi](/#/auth) dengan memasukkan kode akses.",
- },
+ Unauthorized: "Akses tidak diizinkan, silakan masukkan kode akses atau masukkan kunci API OpenAI Anda. di halaman [autentikasi](/#/auth) atau di halaman [Pengaturan](/#/settings).",
+ },
Auth: {
Title: "Diperlukan Kode Akses",
Tips: "Masukkan kode akses di bawah",
+ SubTips: "Atau masukkan kunci API OpenAI Anda",
Input: "Kode Akses",
Confirm: "Konfirmasi",
Later: "Nanti",
@@ -301,6 +301,10 @@ const id: PartialLocaleType = {
Failed:
"Gagal menyalin, mohon berikan izin untuk mengakses clipboard atau Clipboard API tidak didukung (Tauri)",
},
+ Download: {
+ Success: "Konten berhasil diunduh ke direktori Anda.",
+ Failed: "Unduhan gagal.",
+ },
Context: {
Toast: (x: any) => `Dengan ${x} promp kontekstual`,
Edit: "Pengaturan Obrolan Saat Ini",
diff --git a/app/locales/tw.ts b/app/locales/tw.ts
index 15f6648e..e9f38d09 100644
--- a/app/locales/tw.ts
+++ b/app/locales/tw.ts
@@ -7,13 +7,13 @@ const tw: PartialLocaleType = {
Unauthorized: "目前您的狀態是未授權,請前往[設定頁面](/#/auth)輸入授權碼。",
},
ChatItem: {
- ChatItemCount: (count: number) => `${count} 條對話`,
+ ChatItemCount: (count: number) => `${count} 則對話`,
},
Chat: {
- SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 條對話`,
+ SubTitle: (count: number) => `您已經與 ChatGPT 進行了 ${count} 則對話`,
Actions: {
- ChatList: "查看訊息列表",
- CompressedHistory: "查看壓縮後的歷史 Prompt",
+ ChatList: "檢視訊息列表",
+ CompressedHistory: "檢視壓縮後的歷史 Prompt",
Export: "匯出聊天紀錄",
Copy: "複製",
Stop: "停止",
@@ -23,15 +23,15 @@ const tw: PartialLocaleType = {
Rename: "重新命名對話",
Typing: "正在輸入…",
Input: (submitKey: string) => {
- var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可發送`;
+ var inputHints = `輸入訊息後,按下 ${submitKey} 鍵即可傳送`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ",Shift + Enter 鍵換行";
}
return inputHints;
},
- Send: "發送",
+ Send: "傳送",
Config: {
- Reset: "重置預設",
+ Reset: "重設",
SaveAs: "另存新檔",
},
},
@@ -46,7 +46,7 @@ const tw: PartialLocaleType = {
Title: "上下文記憶 Prompt",
EmptyContent: "尚未記憶",
Copy: "複製全部",
- Send: "發送記憶",
+ Send: "傳送記憶",
Reset: "重設對話",
ResetConfirm: "重設後將清除目前對話記錄以及歷史記憶,確認重設?",
},
@@ -71,22 +71,22 @@ const tw: PartialLocaleType = {
},
InjectSystemPrompts: {
Title: "匯入系統提示",
- SubTitle: "強制在每個請求的訊息列表開頭添加一個模擬 ChatGPT 的系統提示",
+ SubTitle: "強制在每個請求的訊息列表開頭新增一個模擬 ChatGPT 的系統提示",
},
Update: {
- Version: (x: string) => `當前版本:${x}`,
+ Version: (x: string) => `目前版本:${x}`,
IsLatest: "已是最新版本",
CheckUpdate: "檢查更新",
IsChecking: "正在檢查更新...",
FoundUpdate: (x: string) => `發現新版本:${x}`,
GoToUpdate: "前往更新",
},
- SendKey: "發送鍵",
+ SendKey: "傳送鍵",
Theme: "主題",
TightBorder: "緊湊邊框",
SendPreviewBubble: {
Title: "預覽氣泡",
- SubTitle: "在預覽氣泡中預覽 Markdown 内容",
+ SubTitle: "在預覽氣泡中預覽 Markdown 內容",
},
Mask: {
Splash: {
@@ -101,7 +101,7 @@ const tw: PartialLocaleType = {
},
List: "自定義提示詞列表",
ListCount: (builtin: number, custom: number) =>
- `內建 ${builtin} 條,用戶定義 ${custom} 條`,
+ `內建 ${builtin} 條,使用者定義 ${custom} 條`,
Edit: "編輯",
Modal: {
Title: "提示詞列表",
@@ -132,7 +132,7 @@ const tw: PartialLocaleType = {
},
IsChecking: "正在檢查…",
Check: "重新檢查",
- NoAccess: "輸入API Key查看餘額",
+ NoAccess: "輸入 API Key 檢視餘額",
},
AccessCode: {
Title: "授權碼",
@@ -150,7 +150,7 @@ const tw: PartialLocaleType = {
},
PresencePenalty: {
Title: "話題新穎度 (presence_penalty)",
- SubTitle: "值越大,越有可能擴展到新話題",
+ SubTitle: "值越大,越有可能拓展到新話題",
},
FrequencyPenalty: {
Title: "頻率懲罰度 (frequency_penalty)",
@@ -163,7 +163,7 @@ const tw: PartialLocaleType = {
Error: "出錯了,請稍後再嘗試",
Prompt: {
History: (content: string) =>
- "這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
+ "這是 AI 與使用者的歷史聊天總結,作為前情提要:" + content,
Topic:
"Use the language used by the user (e.g. en for english conversation, zh-hant for chinese conversation, etc.) to generate a title (at most 6 words) summarizing our conversation without any lead-in, quotation marks, preamble like 'Title:', direct text copies, single-word replies, quotation marks, translations, or brackets. Remove enclosing quotation marks. The title should make third-party grasp the essence of the conversation in first sight.",
Summarize:
@@ -192,16 +192,16 @@ const tw: PartialLocaleType = {
Item: {
Info: (count: number) => `包含 ${count} 條預設對話`,
Chat: "對話",
- View: "查看",
+ View: "檢視",
Edit: "編輯",
- Delete: "删除",
- DeleteConfirm: "確認删除?",
+ Delete: "刪除",
+ DeleteConfirm: "確認刪除?",
},
EditModal: {
Title: (readonly: boolean) =>
- `編輯預設面具 ${readonly ? "(只读)" : ""}`,
+ `編輯預設面具 ${readonly ? "(只讀)" : ""}`,
Download: "下載預設",
- Clone: "克隆預設",
+ Clone: "複製預設",
},
Config: {
Avatar: "角色頭像",
@@ -215,7 +215,7 @@ const tw: PartialLocaleType = {
SubTitle: "現在開始,與面具背後的靈魂思維碰撞",
More: "搜尋更多",
NotShow: "不再呈現",
- ConfirmNoShow: "確認禁用?禁用後可以随時在設定中重新啟用。",
+ ConfirmNoShow: "確認停用?停用後可以隨時在設定中重新啟用。",
},
UI: {
Confirm: "確認",
diff --git a/app/store/config.ts b/app/store/config.ts
index ca230cc3..184355c9 100644
--- a/app/store/config.ts
+++ b/app/store/config.ts
@@ -1,4 +1,5 @@
import { LLMModel } from "../client/api";
+import { isMacOS } from "../utils";
import { getClientConfig } from "../config/client";
import {
DEFAULT_INPUT_TEMPLATE,
@@ -27,7 +28,7 @@ export enum Theme {
export const DEFAULT_CONFIG = {
lastUpdate: Date.now(), // timestamp, to merge state
- submitKey: SubmitKey.CtrlEnter as SubmitKey,
+ submitKey: isMacOS() ? SubmitKey.MetaEnter : SubmitKey.CtrlEnter,
avatar: "1f603",
fontSize: 14,
theme: Theme.Auto as Theme,
diff --git a/app/store/sync.ts b/app/store/sync.ts
index c194162f..c34ae7b9 100644
--- a/app/store/sync.ts
+++ b/app/store/sync.ts
@@ -1,3 +1,4 @@
+import { getClientConfig } from "../config/client";
import { Updater } from "../typing";
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
import { createPersistStore } from "../utils/store";
@@ -20,6 +21,7 @@ export interface WebDavConfig {
password: string;
}
+const isApp = !!getClientConfig()?.isApp;
export type SyncStore = GetStoreState;
const DEFAULT_SYNC_STATE = {
@@ -57,7 +59,11 @@ export const useSyncStore = createPersistStore(
export() {
const state = getLocalAppState();
- const fileName = `Backup-${new Date().toLocaleString()}.json`;
+ const datePart = isApp
+ ? `${new Date().toLocaleDateString().replace(/\//g, '_')} ${new Date().toLocaleTimeString().replace(/:/g, '_')}`
+ : new Date().toLocaleString();
+
+ const fileName = `Backup-${datePart}.json`;
downloadAs(JSON.stringify(state), fileName);
},
diff --git a/app/store/update.ts b/app/store/update.ts
index 42b86586..2b088a13 100644
--- a/app/store/update.ts
+++ b/app/store/update.ts
@@ -2,8 +2,11 @@ import { FETCH_COMMIT_URL, FETCH_TAG_URL, StoreKey } from "../constant";
import { api } from "../client/api";
import { getClientConfig } from "../config/client";
import { createPersistStore } from "../utils/store";
+import ChatGptIcon from "../icons/chatgpt.png";
+import Locale from "../locales";
const ONE_MINUTE = 60 * 1000;
+const isApp = !!getClientConfig()?.isApp;
function formatVersionDate(t: string) {
const d = new Date(+t);
@@ -80,6 +83,38 @@ export const useUpdateStore = createPersistStore(
set(() => ({
remoteVersion: remoteId,
}));
+ if (window.__TAURI__?.notification && isApp) {
+ // Check if notification permission is granted
+ await window.__TAURI__?.notification.isPermissionGranted().then((granted) => {
+ if (!granted) {
+ return;
+ } else {
+ // Request permission to show notifications
+ window.__TAURI__?.notification.requestPermission().then((permission) => {
+ if (permission === 'granted') {
+ if (version === remoteId) {
+ // Show a notification using Tauri
+ window.__TAURI__?.notification.sendNotification({
+ title: "ChatGPT Next Web",
+ body: `${Locale.Settings.Update.IsLatest}`,
+ icon: `${ChatGptIcon.src}`,
+ sound: "Default"
+ });
+ } else {
+ const updateMessage = Locale.Settings.Update.FoundUpdate(`${remoteId}`);
+ // Show a notification for the new version using Tauri
+ window.__TAURI__?.notification.sendNotification({
+ title: "ChatGPT Next Web",
+ body: updateMessage,
+ icon: `${ChatGptIcon.src}`,
+ sound: "Default"
+ });
+ }
+ }
+ });
+ }
+ });
+ }
console.log("[Got Upstream] ", remoteId);
} catch (error) {
console.error("[Fetch Upstream Commit Id]", error);
diff --git a/app/utils.ts b/app/utils.ts
index 37c17dd7..acc140ac 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -31,12 +31,41 @@ export async function copyToClipboard(text: string) {
}
}
-export function downloadAs(text: string, filename: string) {
- const element = document.createElement("a");
- element.setAttribute(
- "href",
- "data:text/plain;charset=utf-8," + encodeURIComponent(text),
- );
+export async function downloadAs(text: string, filename: string) {
+ if (window.__TAURI__) {
+ const result = await window.__TAURI__.dialog.save({
+ defaultPath: `${filename}`,
+ filters: [
+ {
+ name: `${filename.split('.').pop()} files`,
+ extensions: [`${filename.split('.').pop()}`],
+ },
+ {
+ name: "All Files",
+ extensions: ["*"],
+ },
+ ],
+ });
+
+ if (result !== null) {
+ try {
+ await window.__TAURI__.fs.writeBinaryFile(
+ result,
+ new Uint8Array([...text].map((c) => c.charCodeAt(0)))
+ );
+ showToast(Locale.Download.Success);
+ } catch (error) {
+ showToast(Locale.Download.Failed);
+ }
+ } else {
+ showToast(Locale.Download.Failed);
+ }
+ } else {
+ const element = document.createElement("a");
+ element.setAttribute(
+ "href",
+ "data:text/plain;charset=utf-8," + encodeURIComponent(text),
+ );
element.setAttribute("download", filename);
element.style.display = "none";
@@ -46,7 +75,7 @@ export function downloadAs(text: string, filename: string) {
document.body.removeChild(element);
}
-
+}
export function readFromFile() {
return new Promise((res, rej) => {
const fileInput = document.createElement("input");
@@ -173,3 +202,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
export function getCSSVar(varName: string) {
return getComputedStyle(document.body).getPropertyValue(varName).trim();
}
+
+/**
+ * Detects Macintosh
+ */
+export function isMacOS(): boolean {
+ if (typeof window !== "undefined") {
+ let userAgent = window.navigator.userAgent.toLocaleLowerCase();
+ const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent)
+ return !!macintosh
+ }
+ return false
+}
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index ac5d04e8..fee1c860 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -17,7 +17,7 @@ tauri-build = { version = "1.3.0", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
-tauri = { version = "1.3.0", features = ["clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
+tauri = { version = "1.3.0", features = ["notification-all", "fs-all", "clipboard-all", "dialog-all", "shell-open", "updater", "window-close", "window-hide", "window-maximize", "window-minimize", "window-set-icon", "window-set-ignore-cursor-events", "window-set-resizable", "window-show", "window-start-dragging", "window-unmaximize", "window-unminimize"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
[features]
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 77b02a3b..68f9c07c 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -9,7 +9,7 @@
},
"package": {
"productName": "ChatGPT Next Web",
- "version": "2.9.7"
+ "version": "2.9.8"
},
"tauri": {
"allowlist": {
@@ -44,6 +44,12 @@
"startDragging": true,
"unmaximize": true,
"unminimize": true
+ },
+ "fs": {
+ "all": true
+ },
+ "notification": {
+ "all": true
}
},
"bundle": {