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