Browse Source

Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web

GH Action - Upstream Sync 1 year ago
parent
commit
5933b3d7eb

+ 24 - 6
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 ? (
+        <>
+          <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
+          <input
+            className={styles["auth-input"]}
+            type="password"
+            placeholder={Locale.Settings.Token.Placeholder}
+            value={access.token}
+            onChange={(e) => {
+              access.updateToken(e.currentTarget.value);
+            }}
+          />
+        </>
+      ) : null}
 
       <div className={styles["auth-actions"]}>
         <IconButton
           text={Locale.Auth.Confirm}
           type="primary"
-          onClick={goHome}
+          onClick={goChat}
+        />
+        <IconButton
+          text={Locale.Auth.Later}
+          onClick={() => {
+            resetAccessCode();
+            goHome();
+          }}
         />
-        <IconButton text={Locale.Auth.Later} onClick={() => {
-          resetAccessCode();
-          goHome();
-        }} />
       </div>
     </div>
   );

+ 44 - 14
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 = () => {

+ 12 - 0
app/global.d.ts

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

+ 1 - 0
app/locales/ar.ts

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

+ 1 - 0
app/locales/bn.ts

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

+ 5 - 0
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: "当前对话设置",

+ 5 - 0
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",

+ 7 - 3
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",

+ 22 - 22
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: "確認",

+ 2 - 1
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,

+ 7 - 1
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<typeof useSyncStore>;
 
 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);
     },
 

+ 35 - 0
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);

+ 48 - 7
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<string>((res, rej) => {
     const fileInput = document.createElement("input");
@@ -173,3 +202,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
 export function getCSSVar(varName: string) {
   return getComputedStyle(document.body).getPropertyValue(varName).trim();
 }
+
+/**
+ * Detects Macintosh
+ */
+export function isMacOS(): boolean {
+  if (typeof window !== "undefined") {
+    let userAgent = window.navigator.userAgent.toLocaleLowerCase();
+    const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent)
+    return !!macintosh
+  }
+  return false
+}

+ 1 - 1
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]

+ 7 - 1
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": {