Compare commits

...

65 Commits

Author SHA1 Message Date
lencx
11877a960d v0.10.2 2023-02-02 00:46:33 +08:00
lencx
a107e2b21e release 2023-02-02 00:46:23 +08:00
lencx
62924975a6 fix: export buttons (#286) 2023-02-02 00:39:30 +08:00
lencx
8aeca251e6 fix: shortcuts (#254) 2023-02-02 00:04:38 +08:00
lencx
272ef1cd37 feat: copy to clipboard (#191) 2023-02-01 23:48:35 +08:00
lencx
8eade8e9b9 chore: optim 2023-02-01 00:26:41 +08:00
lencx
171ac94f77 chore: optim 2023-01-30 23:55:43 +08:00
lencx
ffef57e934 fix: export buttons do not work (#274) 2023-01-29 23:25:23 +08:00
lencx
7e499b6b1e Merge pull request #266 from lencx/dev 2023-01-27 23:10:46 +08:00
lencx
a38804139a chore: log 2023-01-27 17:44:01 +08:00
lencx
a939b3442e add visitor 2023-01-27 11:52:10 +08:00
lencx
074afb58c8 Merge pull request #256 from lencx/dev 2023-01-27 00:26:38 +08:00
lencx
f9f173407e refactor: app conf 2023-01-27 00:23:05 +08:00
lencx
99877209a1 Merge pull request #250 from lencx/dev 2023-01-25 20:30:06 +08:00
lencx
f0ff062b21 v0.10.1 2023-01-25 19:21:10 +08:00
lencx
ad722b09cf readme 2023-01-25 19:20:47 +08:00
lencx
d78f23d7e2 action 2023-01-25 19:19:13 +08:00
lencx
0c68599d35 fix: zoom level (#202) 2023-01-25 19:09:22 +08:00
lencx
ca171b7c1b fix: program exception when Awesome data is empty (#248) 2023-01-25 16:49:20 +08:00
lencx
7aa70c83de Merge branch 'main' into dev 2023-01-25 11:46:08 +08:00
lencx
585582244d readme 2023-01-25 10:32:19 +08:00
lencx
1c3b3c1f22 readme 2023-01-25 10:26:53 +08:00
lencx
db7a4f0b01 chore: fmt 2023-01-25 10:20:50 +08:00
lencx
655dda8efb Merge pull request #246 from lencx/dev 2023-01-25 03:08:32 +08:00
lencx
f09204ff5a fix: build error 2023-01-25 02:27:38 +08:00
lencx
817cd6f87c fix: build error 2023-01-25 02:09:23 +08:00
lencx
c55ac1df3b v0.10.0 2023-01-25 01:53:45 +08:00
lencx
9ba3357a58 chore: tray 2023-01-25 01:52:37 +08:00
lencx
221287bbd5 readme 2023-01-25 00:54:56 +08:00
lencx
3fea94f669 chore: markdown style 2023-01-25 00:50:58 +08:00
lencx
9a5c008a25 chore: script 2023-01-25 00:38:36 +08:00
lencx
11e20e7f8c Merge branch 'main' into dev 2023-01-25 00:32:18 +08:00
lencx
f0e46639d0 Merge pull request #240 from agurodriguez/fix-nav-padding 2023-01-25 00:31:29 +08:00
lencx
7d602e01a9 update log 2023-01-25 00:26:40 +08:00
lencx
e6052152ea chore: dashboard 2023-01-24 23:23:52 +08:00
lencx
ba438b0640 chore: dashboard 2023-01-24 18:34:31 +08:00
lencx
4a7ee4dcf5 fix: windows path (#242) 2023-01-24 16:24:49 +08:00
lencx
f1e528d3a7 chore: dashboard 2023-01-24 13:23:06 +08:00
lencx
1f573102d3 chore: about 2023-01-23 20:29:05 +08:00
lencx
d5df706b47 chore: settings 2023-01-23 10:56:07 +08:00
Agu Rodríguez
6cf7da5557 Add padding to navbar to avoid overlap with window control buttons 2023-01-22 20:26:42 -05:00
lencx
84a29d7cda chore: window 2023-01-23 00:36:39 +08:00
lencx
f1ec7c6495 chore: remove lint 2023-01-22 20:17:51 +08:00
lencx
6a7eabf5cb chore: husky 2023-01-22 20:16:14 +08:00
lencx
d84c11319d chore: pretty 2023-01-22 18:23:28 +08:00
lencx
bc39dcdd72 chore: add pretty 2023-01-22 18:18:36 +08:00
lencx
1ba356a91f Merge branch 'main' into dev 2023-01-22 15:03:04 +08:00
lencx
c5ba0f783a readme 2023-01-22 15:01:08 +08:00
lencx
c508633262 chore: add lint 2023-01-22 14:48:01 +08:00
lencx
1d7bb3e051 chore: add lint 2023-01-22 14:46:04 +08:00
lencx
b875727753 chore: settings 2023-01-22 11:43:56 +08:00
lencx
1e5ec6028d chore: fmt 2023-01-21 22:51:16 +08:00
lencx
5b6a69444e chore: fmt 2023-01-21 13:25:27 +08:00
lencx
321007bb87 chore: add rustfmt.toml 2023-01-21 12:59:52 +08:00
lencx
5f1c33d750 chore: awesome 2023-01-20 21:28:57 +08:00
lencx
1af173cb24 chore: markdown 2023-01-19 00:13:10 +08:00
lencx
8a3ccb6231 Merge branch 'main' into dev 2023-01-18 22:20:46 +08:00
lencx
e868080a69 readme 2023-01-18 10:00:36 +08:00
lencx
a7d12bafc0 chore: path 2023-01-18 00:03:45 +08:00
lencx
f38d683f4e chore: optim 2023-01-17 22:52:15 +08:00
lencx
240c88220d chore: deps 2023-01-16 21:57:00 +08:00
lencx
f822a56993 chore: markdown 2023-01-16 21:10:38 +08:00
lencx
fb88ba8a1f readme 2023-01-16 21:07:06 +08:00
lencx
3ffd832640 Merge pull request #214 from lencx/dev 2023-01-15 18:35:57 +08:00
lencx
18c6b1e4ad Merge pull request #212 from lencx/dev 2023-01-15 16:43:33 +08:00
86 changed files with 3991 additions and 2381 deletions

View File

@@ -1,12 +1,12 @@
name: "🕷️ Bug report" name: '🕷️ Bug report'
description: "report bugs" description: 'report bugs'
title: "[Bug]" title: '[Bug]'
labels: labels:
- "bug" - 'bug'
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before filing a new one!" value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before filing a new one!'
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
@@ -16,8 +16,8 @@ body:
id: version id: version
attributes: attributes:
label: Version label: Version
description: "Please specify the version of ChatGPT you are using, a newer version may have fixed the bug you encountered.Check the [UPDATE_LOG](https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md) for more information." description: 'Please specify the version of ChatGPT you are using, a newer version may have fixed the bug you encountered.Check the [UPDATE_LOG](https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md) for more information.'
placeholder: "e.g. v0.1.0" placeholder: 'e.g. v0.1.0'
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -32,12 +32,12 @@ body:
id: OS id: OS
attributes: attributes:
label: OS label: OS
description: "Please specify the OS you are using." description: 'Please specify the OS you are using.'
placeholder: "e.g. Ubuntu 22.04" placeholder: 'e.g. Ubuntu 22.04'
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: environment id: environment
attributes: attributes:
label: Environment label: Environment
description: "If you think your environment may be related to the problem, please describe it here." description: 'If you think your environment may be related to the problem, please describe it here.'

View File

@@ -1,15 +1,15 @@
name: "❌ Build error report" name: '❌ Build error report'
description: "report errors when building by yourself" description: 'report errors when building by yourself'
title: "[Build Error]" title: '[Build Error]'
labels: labels:
- "build error" - 'build error'
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before filing a new one!" value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before filing a new one!'
- type: markdown - type: markdown
attributes: attributes:
value: "Please make sure to build from the source code with the latest version of ChatGPT." value: 'Please make sure to build from the source code with the latest version of ChatGPT.'
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
@@ -19,19 +19,19 @@ body:
id: error id: error
attributes: attributes:
label: Error message label: Error message
description: "Please paste the error message here." description: 'Please paste the error message here.'
validations: validations:
required: true required: true
- type: input - type: input
id: OS id: OS
attributes: attributes:
label: OS label: OS
description: "Please specify the OS you are using." description: 'Please specify the OS you are using.'
placeholder: "e.g. Ubuntu 22.04" placeholder: 'e.g. Ubuntu 22.04'
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: environment id: environment
attributes: attributes:
label: Environment label: Environment
description: "If you think your environment may be related to the problem, please describe it here." description: 'If you think your environment may be related to the problem, please describe it here.'

View File

@@ -1,17 +1,17 @@
name: "📚 Documentation Issue" name: '📚 Documentation Issue'
description: "report documentation issues, typos welcome!" description: 'report documentation issues, typos welcome!'
title: "[Doc]" title: '[Doc]'
labels: labels:
- "documentation" - 'documentation'
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one." value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one.'
- type: textarea - type: textarea
id: doc-description id: doc-description
attributes: attributes:
label: "Provide a description of requested docs changes" label: 'Provide a description of requested docs changes'
description: "Briefly describe the requested docs changes." description: 'Briefly describe the requested docs changes.'
validations: validations:
required: true required: true
- type: markdown - type: markdown

View File

@@ -1,34 +1,34 @@
name: "⭐ Feature or enhancement request" name: '⭐ Feature or enhancement request'
description: "suggest new features or enhancements" description: 'suggest new features or enhancements'
title: "[Feature]" title: '[Feature]'
labels: labels:
- "enhancement" - 'enhancement'
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one." value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one.'
- type: textarea - type: textarea
id: feature-description id: feature-description
attributes: attributes:
label: "Feature description" label: 'Feature description'
description: "Describe the feature or enhancements you'd like to see." description: "Describe the feature or enhancements you'd like to see."
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: motivation id: motivation
attributes: attributes:
label: "Motivation" label: 'Motivation'
description: "Describe the motivation for this feature or enhancement." description: 'Describe the motivation for this feature or enhancement.'
- type: textarea - type: textarea
id: alternatives id: alternatives
attributes: attributes:
label: "Alternatives" label: 'Alternatives'
description: "Describe any alternatives you've considered." description: "Describe any alternatives you've considered."
- type: textarea - type: textarea
id: additional-context id: additional-context
attributes: attributes:
label: "Additional context" label: 'Additional context'
description: "Add any other context or screenshots about the feature request here." description: 'Add any other context or screenshots about the feature request here.'
- type: markdown - type: markdown
attributes: attributes:
value: Please limit one request per issue. value: Please limit one request per issue.

View File

@@ -1,34 +1,34 @@
name: "⚠️ Security&Privacy issue" name: '⚠️ Security&Privacy issue'
description: "Report security or privacy issues" description: 'Report security or privacy issues'
title: "[Security]" title: '[Security]'
labels: labels:
- "security" - 'security'
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: "Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one." value: 'Please make sure to [search for existing issues](https://github.com/lencx/ChatGPT/issues) before creating a new one.'
- type: textarea - type: textarea
id: security-description id: security-description
attributes: attributes:
label: "Description" label: 'Description'
description: "Describe the security or privacy issue." description: 'Describe the security or privacy issue.'
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: motivation id: motivation
attributes: attributes:
label: "Motivation" label: 'Motivation'
description: "Describe the motivation for this security or privacy issue." description: 'Describe the motivation for this security or privacy issue.'
- type: textarea - type: textarea
id: alternatives id: alternatives
attributes: attributes:
label: "Alternatives" label: 'Alternatives'
description: "Describe any alternatives you've considered." description: "Describe any alternatives you've considered."
- type: textarea - type: textarea
id: additional-context id: additional-context
attributes: attributes:
label: "Additional context" label: 'Additional context'
description: "Add any other context or screenshots about the security or privacy issue here." description: 'Add any other context or screenshots about the security or privacy issue here.'
- type: markdown - type: markdown
attributes: attributes:
value: Please limit one request per issue. value: Please limit one request per issue.

6
.husky/pre-commit Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run pretty-quick
cargo fmt
git add .

45
.prettierignore Normal file
View File

@@ -0,0 +1,45 @@
package-lock.json
node_modules/
yarn.lock
*.lock
casks/
# rust
src-tauri/
target/
Cargo.lock
*.toml
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
assets/**
public/**
.gitattributes
.gitignore
.prettierignore
LICENSE

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "all",
"singleQuote": true,
"semi": true,
"tabWidth": 2,
"printWidth": 100
}

View File

@@ -1,26 +0,0 @@
# Awesome ChatGPT
- [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) - This repo includes ChatGPT prompt curation to use ChatGPT better.
- [Awesome ChatGPT](https://github.com/humanloop/awesome-chatgpt) - Curated list of awesome tools, demos, docs for ChatGPT and GPT-3
## Extension
`Browser`
- [ChatGPT Export and Share](https://github.com/liady/ChatGPT-pdf) - A Chrome extension for downloading your ChatGPT history to PNG, PDF or creating a sharable link
- [ChatGPT for Google](https://github.com/wong2/chat-gpt-google-extension) - A browser extension to display ChatGPT response alongside Google Search results
- [ChatGPT Extension](https://github.com/kazuki-sf/ChatGPT_Extension) - ChatGPT Extension is a really simple Chrome Extension (manifest v3) that you can access OpenAI's ChatGPT from anywhere on the web.
- [ChatGPT-Google](https://github.com/ZohaibAhmed/ChatGPT-Google) - Chrome Extension that Integrates ChatGPT (Unofficial) into Google Search
`VSCode`
- [ChatGPT Extension for VSCode](https://github.com/mpociot/chatgpt-vscode) - A VSCode extension that allows you to use ChatGPT
`Bot`
- [ChatGPT Telegram Bot](https://github.com/altryne/chatGPT-telegram-bot) - This is a very early attempt at having chatGPT work within a telegram bot
## Tools
- [commitgpt](https://github.com/RomanHotsiy/commitgpt) - Automatically generate commit messages using ChatGPT
- [ShareGPT](https://sharegpt.com/) - ShareGPT: Share your wildest ChatGPT conversations with one click.

9
Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[workspace]
members = ["src-tauri"]
# fix: mac v1.2.0 can not copy/paste
# https://github.com/tauri-apps/tauri/issues/5669
[profile.release]
strip = true
lto = true
opt-level = "s"

View File

@@ -9,12 +9,12 @@
![License](https://img.shields.io/badge/License-Apache%202-green.svg) ![License](https://img.shields.io/badge/License-Apache%202-green.svg)
[![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases) [![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases)
[![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr) [![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr)
[![lencx](https://img.shields.io/twitter/follow/lencx_.svg?style=social)](https://twitter.com/lencx_) [![lencx](https://img.shields.io/badge/follow-lencx__-blue?style=flat&logo=Twitter)](https://twitter.com/lencx_)
<!-- [![lencx](https://img.shields.io/twitter/follow/lencx_.svg?style=social)](https://twitter.com/lencx_) -->
<a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 145px !important;" ></a> <a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 145px !important;" ></a>
[支持者福利 (仅会员可见)](https://www.buymeacoffee.com/lencx/posts) - 分享 ChatGPT 桌面应用使用技巧以及下一步计划。
## 📦 安装 ## 📦 安装
- [📝 更新日志](./UPDATE_LOG.md) - [📝 更新日志](./UPDATE_LOG.md)
@@ -24,22 +24,23 @@
### Windows ### Windows
- [ChatGPT_0.9.2_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/ChatGPT_0.9.2_x64_en-US.msi): - [ChatGPT_0.10.2_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/ChatGPT_0.10.2_x64_en-US.msi):
- 使用 [winget](https://winstall.app/apps/lencx.ChatGPT): - 使用 [winget](https://winstall.app/apps/lencx.ChatGPT):
```bash ```bash
# install the latest version # install the latest version
winget install --id=lencx.ChatGPT -e winget install --id=lencx.ChatGPT -e
# install the specified version # install the specified version
winget install --id=lencx.ChatGPT -e --version 0.9.0 winget install --id=lencx.ChatGPT -e --version 0.10.0
``` ```
**注意:如果安装路径和应用名称相同,会导致冲突 ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.9.2))** **注意:如果安装路径和应用名称相同,会导致冲突 ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.10.2))**
### Mac ### Mac
- [ChatGPT_0.9.2_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/ChatGPT_0.9.2_x64.dmg) - [ChatGPT_0.10.2_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/ChatGPT_0.10.2_x64.dmg)
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/ChatGPT.app.tar.gz) - [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/ChatGPT.app.tar.gz)
- Homebrew \ - Homebrew \
_[Homebrew 快捷安装](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_ _[Homebrew 快捷安装](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
```sh ```sh
@@ -55,8 +56,8 @@
### Linux ### Linux
- [chat-gpt_0.9.2_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/chat-gpt_0.9.2_amd64.deb) - [chat-gpt_0.10.2_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/chat-gpt_0.10.2_amd64.deb)
- [chat-gpt_0.9.2_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/chat-gpt_0.9.2_amd64.AppImage): **工作可靠,`.deb` 运行失败时可以尝试它** - [chat-gpt_0.10.2_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/chat-gpt_0.10.2_amd64.AppImage): **工作可靠,`.deb` 运行失败时可以尝试它**
- 使用 [AUR](https://aur.archlinux.org/packages/chatgpt-desktop-bin): - 使用 [AUR](https://aur.archlinux.org/packages/chatgpt-desktop-bin):
```bash ```bash
yay -S chatgpt-desktop-bin yay -S chatgpt-desktop-bin
@@ -72,7 +73,7 @@
你可以从 [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 来寻找有趣的功能来导入到应用。也可以使用 `Sync Prompts`,来一键同步所有,如果你不想让某些提示出现在你的斜杠命令,你可以禁用它们。 你可以从 [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 来寻找有趣的功能来导入到应用。也可以使用 `Sync Prompts`,来一键同步所有,如果你不想让某些提示出现在你的斜杠命令,你可以禁用它们。
![chatgpt menu](./assets/chatgpt-menu.png) ![chatgpt cmd](./assets/chatgpt-cmd.png)
![chatgpt sync prompts](./assets/chatgpt-sync-prompts.png) ![chatgpt sync prompts](./assets/chatgpt-sync-prompts.png)
<!-- 数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。 --> <!-- 数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。 -->
@@ -86,6 +87,7 @@
- 跨平台: `macOS` `Linux` `Windows` - 跨平台: `macOS` `Linux` `Windows`
- 导出 ChatGPT 聊天记录 (支持 PNG, PDF 和生成分享链接) - 导出 ChatGPT 聊天记录 (支持 PNG, PDF 和生成分享链接)
- 主窗口和系统托盘支持自定义 URL将任意网站包装成一个桌面应用
- 应用自动升级通知 - 应用自动升级通知
- 丰富的快捷键 - 丰富的快捷键
- 系统托盘悬浮窗 - 系统托盘悬浮窗
@@ -128,6 +130,7 @@
- `[.chatgpt]` - 应用配置根路径 - `[.chatgpt]` - 应用配置根路径
- `chat.conf.json` - 应用喜好配置 - `chat.conf.json` - 应用喜好配置
- `chat.awesome.json` - 自定义 URL 列表,类似于浏览器书签。可以将任意 URL 作为主窗口或托盘窗口 (**Control Conter -> Awesome**)
- `chat.model.json` - ChatGPT 输入提示,通过斜杠命令来快速完成输入,主要包含三部分: - `chat.model.json` - ChatGPT 输入提示,通过斜杠命令来快速完成输入,主要包含三部分:
- `user_custom` - 需要手动录入 (**Control Conter -> Language Model -> User Custom**) - `user_custom` - 需要手动录入 (**Control Conter -> Language Model -> User Custom**)
- `sync_prompts` - 从 [f/awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 同步数据 (**Control Conter -> Language Model -> Sync Prompts**) - `sync_prompts` - 从 [f/awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 同步数据 (**Control Conter -> Language Model -> Sync Prompts**)

View File

@@ -7,16 +7,17 @@
[![English badge](https://img.shields.io/badge/%E8%8B%B1%E6%96%87-English-blue)](./README.md) [![English badge](https://img.shields.io/badge/%E8%8B%B1%E6%96%87-English-blue)](./README.md)
[![简体中文 badge](https://img.shields.io/badge/%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-Simplified%20Chinese-blue)](./README-ZH_CN.md)\ [![简体中文 badge](https://img.shields.io/badge/%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-Simplified%20Chinese-blue)](./README-ZH_CN.md)\
![License](https://img.shields.io/badge/License-Apache%202-green.svg) ![License](https://img.shields.io/badge/License-Apache%202-green.svg)
![visitor](https://visitor-badge.glitch.me/badge?page_id=lencx.chatgpt)
[![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases) [![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases)
[![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr) [![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr)
[![lencx](https://img.shields.io/twitter/follow/lencx_.svg?style=social)](https://twitter.com/lencx_) [![lencx](https://img.shields.io/badge/follow-lencx__-blue?style=flat&logo=Twitter)](https://twitter.com/lencx_)
<!-- [![lencx](https://img.shields.io/twitter/follow/lencx_.svg?style=social)](https://twitter.com/lencx_) -->
<!-- [![中文版 badge](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-Traditional%20Chinese-blue)](./README-ZH.md) --> <!-- [![中文版 badge](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-Traditional%20Chinese-blue)](./README-ZH.md) -->
<a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 145px !important;" ></a> <a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 145px !important;" ></a>
[Supporter Benefits (visible to your supporters and members only)](https://www.buymeacoffee.com/lencx/posts) - Share tips for using ChatGPT desktop application and next steps.
## 📦 Install ## 📦 Install
- [📝 Update Log](./UPDATE_LOG.md) - [📝 Update Log](./UPDATE_LOG.md)
@@ -26,22 +27,23 @@
### Windows ### Windows
- [ChatGPT_0.9.2_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/ChatGPT_0.9.2_x64_en-US.msi): Direct download installer - [ChatGPT_0.10.2_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/ChatGPT_0.10.2_x64_en-US.msi): Direct download installer
- Use [winget](https://winstall.app/apps/lencx.ChatGPT): - Use [winget](https://winstall.app/apps/lencx.ChatGPT):
```bash ```bash
# install the latest version # install the latest version
winget install --id=lencx.ChatGPT -e winget install --id=lencx.ChatGPT -e
# install the specified version # install the specified version
winget install --id=lencx.ChatGPT -e --version 0.9.0 winget install --id=lencx.ChatGPT -e --version 0.10.0
``` ```
**Note: If the installation path and application name are the same, it will lead to conflict ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.9.2))** **Note: If the installation path and application name are the same, it will lead to conflict ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.10.2))**
### Mac ### Mac
- [ChatGPT_0.9.2_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/ChatGPT_0.9.2_x64.dmg): Direct download installer - [ChatGPT_0.10.2_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/ChatGPT_0.10.2_x64.dmg): Direct download installer
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/ChatGPT.app.tar.gz): Download the `.app` installer - [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/ChatGPT.app.tar.gz): Download the `.app` installer
- Homebrew \ - Homebrew \
Or you can install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_ Or you can install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
```sh ```sh
@@ -57,8 +59,8 @@
### Linux ### Linux
- [chat-gpt_0.9.2_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/chat-gpt_0.9.2_amd64.deb): Download `.deb` installer, advantage small size, disadvantage poor compatibility - [chat-gpt_0.10.2_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/chat-gpt_0.10.2_amd64.deb): Download `.deb` installer, advantage small size, disadvantage poor compatibility
- [chat-gpt_0.9.2_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.9.2/chat-gpt_0.9.2_amd64.AppImage): Works reliably, you can try it if `.deb` fails to run - [chat-gpt_0.10.2_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.10.2/chat-gpt_0.10.2_amd64.AppImage): Works reliably, you can try it if `.deb` fails to run
- Available on [AUR](https://aur.archlinux.org/packages/chatgpt-desktop-bin) with the package name `chatgpt-desktop-bin`, and you can use your favourite AUR package manager to install it. - Available on [AUR](https://aur.archlinux.org/packages/chatgpt-desktop-bin) with the package name `chatgpt-desktop-bin`, and you can use your favourite AUR package manager to install it.
<!-- download end --> <!-- download end -->
@@ -73,7 +75,7 @@ This is a major and exciting update. It works like a `Telegram bot command` and
You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)** to find interesting features to import into the app. You can also use `Sync Prompts` to sync all in one click, and if you don't want certain prompts to appear in your slash commands, you can disable them. You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)** to find interesting features to import into the app. You can also use `Sync Prompts` to sync all in one click, and if you don't want certain prompts to appear in your slash commands, you can disable them.
![chatgpt menu](./assets/chatgpt-menu.png) ![chatgpt cmd](./assets/chatgpt-cmd.png)
![chatgpt sync prompts](./assets/chatgpt-sync-prompts.png) ![chatgpt sync prompts](./assets/chatgpt-sync-prompts.png)
<!-- After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`). --> <!-- After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`). -->
@@ -89,6 +91,7 @@ You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt
- Multi-platform: `macOS` `Linux` `Windows` - Multi-platform: `macOS` `Linux` `Windows`
- Export ChatGPT history (PNG, PDF and Markdown) - Export ChatGPT history (PNG, PDF and Markdown)
- The main window and system tray support custom URLs to wrap any website into a desktop application
- Automatic application upgrade notification - Automatic application upgrade notification
- Common shortcut keys - Common shortcut keys
- System tray hover window - System tray hover window
@@ -131,6 +134,7 @@ You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt
- `[.chatgpt]` - application configuration root folder - `[.chatgpt]` - application configuration root folder
- `chat.conf.json` - preferences configuration - `chat.conf.json` - preferences configuration
- `chat.awesome.json` - Custom URL lists, similar to browser bookmarks. Any URL can be used as the main window or tray window (**Control Conter -> Awesome**)
- `chat.model.json` - prompts configurationcontains three parts: - `chat.model.json` - prompts configurationcontains three parts:
- `user_custom` - Requires manual data entry (**Control Conter -> Language Model -> User Custom**) - `user_custom` - Requires manual data entry (**Control Conter -> Language Model -> User Custom**)
- `sync_prompts` - Synchronizing data from [f/awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) (**Control Conter -> Language Model -> Sync Prompts**) - `sync_prompts` - Synchronizing data from [f/awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) (**Control Conter -> Language Model -> Sync Prompts**)
@@ -175,6 +179,7 @@ Currently, only json and csv are supported for synchronizing custom files, and t
## 📌 TODO ## 📌 TODO
<!-- - Web access capability ([#20](https://github.com/lencx/ChatGPT/issues/20)) --> <!-- - Web access capability ([#20](https://github.com/lencx/ChatGPT/issues/20)) -->
- `Control Center` enhancement - `Control Center` enhancement
- `Pop-up Search` enhancement - `Pop-up Search` enhancement
- ... - ...

View File

@@ -1,177 +1,234 @@
# UPDATE LOG # UPDATE LOG
## v0.10.2
Fix:
- PNG and PDF buttons do not work (https://github.com/lencx/ChatGPT/issues/274)
- Change the window size and the Send button is obscured by the Export button (https://github.com/lencx/ChatGPT/issues/286)
- Change forward and backward shortcuts (https://github.com/lencx/ChatGPT/issues/254)
- MacOS: `Cmd [`, `Cmd ]`
- Windows and Linux: `Ctrl [`, `Ctrl ]`
Feat:
- Copy a single record to the clipboard (https://github.com/lencx/ChatGPT/issues/191)
## v0.10.1
Fix:
- Program exception when `Awesome` data is empty (https://github.com/lencx/ChatGPT/issues/248)
Feat:
- New shortcut key to change zoom level (30% - 200%), `+` or `-` 10% each time, `0` will be reset to 100% (https://github.com/lencx/ChatGPT/issues/202)
- Windows: `Ctrl +`, `Ctrl -`, `Ctrl 0`
- MacOS: `Cmd +`, `Cmd -`, `Cmd 0`
## v0.10.0
Fix:
- After exporting a file in Windows, open an empty file explorer (https://github.com/lencx/ChatGPT/issues/242)
Feat:
- Markdown files support editing and live preview
- Add `Awesome` menu to the `Control Center` (similar to bookmarks, but it's just a start, more possibilities in the future), custom URL support for the home and tray windows (if you're tired of ChatGPT as your home screen).
## v0.9.2 ## v0.9.2
fix: slash command does not work Fix: Slash command does not work
## v0.9.1 ## v0.9.1
fix: slash command does not work Fix: Slash command does not work
## v0.9.0 ## v0.9.0
fix: Fix:
- export button does not work
feat: - Export button does not work
- add an export markdown button
Feat:
- Add an export markdown button
- `Control Center` adds `Notes` and `Download` menus for managing exported chat files (Markdown, PNG, PDF). `Notes` supports markdown previews. - `Control Center` adds `Notes` and `Download` menus for managing exported chat files (Markdown, PNG, PDF). `Notes` supports markdown previews.
## v0.8.1 ## v0.8.1
fix: Fix:
- export button keeps blinking
- export button in the old chat does not work - Export button keeps blinking
- disable export sharing links because it is a security risk - Export button in the old chat does not work
- Disable export sharing links because it is a security risk
## v0.8.0 ## v0.8.0
feat: Feat:
- theme enhancement (Light, Dark, System)
- automatic updates support `silent` settings
- pop-up search: select the ChatGPT content with the mouse, the `DALL·E 2` button appears, and click to jump (note: because the search content filled by the script cannot trigger the event directly, you need to enter a space in the input box to make the button clickable).
fix: - Theme enhancement (Light, Dark, System)
- close the main window and hide it in the tray (windows systems) - Automatic updates support `silent` settings
- Pop-up search: select the ChatGPT content with the mouse, the `DALL·E 2` button appears, and click to jump (note: because the search content filled by the script cannot trigger the event directly, you need to enter a space in the input box to make the button clickable).
Fix:
- Close the main window and hide it in the tray (windows systems)
## v0.7.4 ## v0.7.4
fix: Fix:
- trying to resolve linux errors: `error while loading shared libraries`
- customize global shortcuts (`Menu -> Preferences -> Control Center -> General -> Global Shortcut`) - Trying to resolve linux errors: `error while loading shared libraries`
- Customize global shortcuts (`Menu -> Preferences -> Control Center -> General -> Global Shortcut`)
## v0.7.3 ## v0.7.3
chore: Chore:
- optimize slash command style
- optimize tray menu icon and button icons - Optimize slash command style
- global shortcuts to the chatgpt app (mac: `Command + Shift + O`, windows: `Ctrl + Shift + O`) - Optimize tray menu icon and button icons
- Global shortcuts to the chatgpt app (mac: `Command + Shift + O`, windows: `Ctrl + Shift + O`)
## v0.7.2 ## v0.7.2
fix: some windows systems cannot start the application Fix: Some windows systems cannot start the application
## v0.7.1 ## v0.7.1
fix: Fix:
- some windows systems cannot start the application
- windows and linux add about menu (show version information) - Some windows systems cannot start the application
- the tray icon is indistinguishable from the background in dark mode on window and linux - Windows and linux add about menu (show version information)
- The tray icon is indistinguishable from the background in dark mode on window and linux
## v0.7.0 ## v0.7.0
fix: Fix:
- mac m1 copy/paste does not work on some system versions
- optimize the save chat log button to a small icon, the tray window no longer provides a save chat log button (the buttons causes the input area to become larger and the content area to become smaller)
feat: - Mac m1 copy/paste does not work on some system versions
- use the keyboard `⇧` (arrow up) and `⇩` (arrow down) keys to select the slash command - Optimize the save chat log button to a small icon, the tray window no longer provides a save chat log button (the buttons causes the input area to become larger and the content area to become smaller)
Feat:
- Use the keyboard `⇧` (arrow up) and `⇩` (arrow down) keys to select the slash command
<!-- - global shortcuts to the chatgpt app (mac: command+shift+o, windows: ctrl+shift+o) --> <!-- - global shortcuts to the chatgpt app (mac: command+shift+o, windows: ctrl+shift+o) -->
## v0.6.10 ## v0.6.10
fix: sync failure on windows Fix: Sync failure on windows
## v0.6.4 ## v0.6.4
fix: path not allowed on the configured scope Fix: Path not allowed on the configured scope
feat: Feat:
- optimize the generated pdf file size
- menu added `Sync Prompts` - Optimize the generated pdf file size
- Menu added `Sync Prompts`
- `Control Center` added `Sync Custom` - `Control Center` added `Sync Custom`
- the slash command is triggered by the enter key - The slash command is triggered by the enter key
- under the slash command, use the tab key to modify the contents of the `{q}` tag (only single changes are supported (https://github.com/lencx/ChatGPT/issues/54) - Under the slash command, use the tab key to modify the contents of the `{q}` tag (only single changes are supported (https://github.com/lencx/ChatGPT/issues/54)
## v0.6.0 ## v0.6.0
fix: Fix:
- windows show Chinese when upgrading
- Windows show Chinese when upgrading
## v0.5.1 ## v0.5.1
some optimization Some optimization
## v0.5.0 ## v0.5.0
feat: `Control Center` added `chatgpt-prompts` synchronization Feat: `Control Center` added `chatgpt-prompts` synchronization
## v0.4.2 ## v0.4.2
add chatgpt log (path: `~/.chatgpt/chatgpt.log`) Add chatgpt log (path: `~/.chatgpt/chatgpt.log`)
## v0.4.1 ## v0.4.1
fix: Fix:
- tray window style optimization
- Tray window style optimization
## v0.4.0 ## v0.4.0
feat: Feat:
- customize the ChatGPT prompts command (https://github.com/lencx/ChatGPT#-announcement)
- menu enhancement: hide application icons from the Dock (support macOS only) - Customize the ChatGPT prompts command (https://github.com/lencx/ChatGPT#-announcement)
- Menu enhancement: hide application icons from the Dock (support macOS only)
## v0.3.0 ## v0.3.0
fix: can't open ChatGPT Fix: Can't open ChatGPT
feat: menu enhancement Feat: Menu enhancement
- the control center of ChatGPT application
- open the configuration file directory - The control center of ChatGPT application
- Open the configuration file directory
## v0.2.2 ## v0.2.2
feat: Feat:
- menu: go to config
- Menu: go to config
## v0.2.1 ## v0.2.1
feat: menu optimization Feat: Menu optimization
## v0.2.0 ## v0.2.0
feat: menu enhancement Feat: Menu enhancement
- customize user-agent to prevent security detection interception
- clear all chatgpt configuration files - Customize user-agent to prevent security detection interception
- Clear all chatgpt configuration files
## v0.1.8 ## v0.1.8
feat: Feat:
- menu enhancement: theme, titlebar
- modify website address - Menu enhancement: theme, titlebar
- Modify website address
## v0.1.7 ## v0.1.7
feat: tray window Feat: Tray window
## v0.1.6 ## v0.1.6
feat: Feat:
- stay on top
- export ChatGPT history - Stay on top
- Export ChatGPT history
## v0.1.5 ## v0.1.5
fix: mac can't use shortcut keys Fix: Mac can't use shortcut keys
## v0.1.4 ## v0.1.4
feat: Feat:
- beautify icons
- add system tray menu - Beautify icons
- Add system tray menu
## v0.1.3 ## v0.1.3
fix: only mac supports `TitleBarStyle` Fix: Only mac supports `TitleBarStyle`
## v0.1.2 ## v0.1.2
initialization Initialization
## v0.1.1 ## v0.1.1
initialization Initialization
## v0.1.0 ## v0.1.0
initialization Initialization

BIN
assets/chatgpt-cmd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 742 KiB

View File

@@ -1,3 +0,0 @@
# ChatGPT Model
- [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts)

View File

@@ -12,8 +12,12 @@
"fix:tray": "tr override --json.tauri_systemTray_iconPath=\"icons/tray-icon-light.png\" --json.tauri_systemTray_iconAsTemplate=false", "fix:tray": "tr override --json.tauri_systemTray_iconPath=\"icons/tray-icon-light.png\" --json.tauri_systemTray_iconAsTemplate=false",
"fix:tray:mac": "tr override --json.tauri_systemTray_iconPath=\"icons/tray-icon.png\" --json.tauri_systemTray_iconAsTemplate=true", "fix:tray:mac": "tr override --json.tauri_systemTray_iconPath=\"icons/tray-icon.png\" --json.tauri_systemTray_iconAsTemplate=true",
"download": "node ./scripts/download.js", "download": "node ./scripts/download.js",
"fmt:rs": "cargo fmt",
"tr": "tr", "tr": "tr",
"tauri": "tauri" "tauri": "tauri",
"prettier": "prettier -c --write '**/*.{js,md,ts,tsx,yml}'",
"pretty-quick": "pretty-quick --staged",
"prepare": "husky install"
}, },
"license": "MIT", "license": "MIT",
"author": "lencx <cxin1314@gmail.com>", "author": "lencx <cxin1314@gmail.com>",
@@ -34,15 +38,22 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@monaco-editor/react": "^4.4.6",
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.2.0",
"antd": "^5.1.0", "antd": "^5.1.0",
"clsx": "^1.2.1",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"github-markdown-css": "^5.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^8.0.4", "react-markdown": "^8.0.4",
"react-resizable-panels": "^0.0.33",
"react-router-dom": "^6.4.5", "react-router-dom": "^6.4.5",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"rehype-raw": "^6.1.1",
"remark-comment-config": "^7.0.1",
"remark-gfm": "^3.0.1",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
@@ -55,6 +66,9 @@
"@types/react-syntax-highlighter": "^15.5.6", "@types/react-syntax-highlighter": "^15.5.6",
"@types/uuid": "^9.0.0", "@types/uuid": "^9.0.0",
"@vitejs/plugin-react": "^3.0.0", "@vitejs/plugin-react": "^3.0.0",
"husky": "^8.0.3",
"prettier": "^2.8.3",
"pretty-quick": "^3.1.3",
"sass": "^1.56.2", "sass": "^1.56.2",
"typescript": "^4.9.4", "typescript": "^4.9.4",
"vite": "^4.0.0", "vite": "^4.0.0",

14
rustfmt.toml Normal file
View File

@@ -0,0 +1,14 @@
max_width = 100
hard_tabs = false
tab_spaces = 2
newline_style = "Auto"
use_small_heuristics = "Default"
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
edition = "2021"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
# imports_granularity = "Crate"

View File

@@ -26,15 +26,10 @@ wry = "0.24.1"
dark-light = "1.0.0" dark-light = "1.0.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.23.0", features = ["macros"] } tokio = { version = "1.23.0", features = ["macros"] }
tauri = { version = "1.2.4", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] }
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] } tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
tauri = { version = "1.2.3", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] } tauri-plugin-log = { git = "https://github.com/lencx/tauri-plugins-workspace", branch = "dev", features = ["colored"] }
[dependencies.tauri-plugin-log] tauri-plugin-autostart = { git = "https://github.com/lencx/tauri-plugins-workspace", branch = "dev" }
git = "https://github.com/lencx/tauri-plugin-log"
branch = "dev"
features = ["colored"]
[dependencies.tauri-plugin-autostart]
git = "https://github.com/lencx/tauri-plugin-autostart"
branch = "dev"
# sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite"] } # sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite"] }
@@ -45,10 +40,3 @@ default = [ "custom-protocol" ]
# this feature is used used for production builds where `devPath` points to the filesystem # this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this # DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ] custom-protocol = [ "tauri/custom-protocol" ]
# fix: mac v1.2.0 can not copy/paste
# https://github.com/tauri-apps/tauri/issues/5669
[profile.release]
strip = true
lto = true
opt-level = "s"

View File

@@ -1,29 +1,13 @@
use crate::{ use crate::utils;
app::{fs_extra, window}, use log::error;
conf::{ChatConfJson, GITHUB_PROMPTS_CSV_URL}, use std::{fs, path::PathBuf};
utils::{self, chat_root, create_file}, use tauri::{api, command, AppHandle, Manager};
};
use log::info;
use regex::Regex;
use std::{collections::HashMap, fs, path::PathBuf, vec};
use tauri::{api, command, AppHandle, Manager, Theme};
use walkdir::WalkDir;
#[command] #[command]
pub fn drag_window(app: AppHandle) { pub fn drag_window(app: AppHandle) {
app.get_window("core").unwrap().start_dragging().unwrap(); app.get_window("core").unwrap().start_dragging().unwrap();
} }
#[command]
pub fn dalle2_window(app: AppHandle, query: String) {
window::dalle2_window(
&app.app_handle(),
Some(query),
Some("ChatGPT & DALL·E 2".to_string()),
None,
);
}
#[command] #[command]
pub fn fullscreen(app: AppHandle) { pub fn fullscreen(app: AppHandle) {
let win = app.get_window("core").unwrap(); let win = app.get_window("core").unwrap();
@@ -35,19 +19,29 @@ pub fn fullscreen(app: AppHandle) {
} }
#[command] #[command]
pub fn download(_app: AppHandle, name: String, blob: Vec<u8>) { pub fn download(app: AppHandle, name: String, blob: Vec<u8>) {
let path = chat_root().join(PathBuf::from(name)); let win = app.app_handle().get_window("core");
create_file(&path).unwrap(); let path = utils::app_root().join(PathBuf::from(name));
utils::create_file(&path).unwrap();
fs::write(&path, blob).unwrap(); fs::write(&path, blob).unwrap();
utils::open_file(path); tauri::api::dialog::message(
win.as_ref(),
"Save File",
format!("PATH: {}", path.display()),
);
} }
#[command] #[command]
pub fn save_file(_app: AppHandle, name: String, content: String) { pub fn save_file(app: AppHandle, name: String, content: String) {
let path = chat_root().join(PathBuf::from(name)); let win = app.app_handle().get_window("core");
create_file(&path).unwrap(); let path = utils::app_root().join(PathBuf::from(name));
utils::create_file(&path).unwrap();
fs::write(&path, content).unwrap(); fs::write(&path, content).unwrap();
utils::open_file(path); tauri::api::dialog::message(
win.as_ref(),
"Save File",
format!("PATH: {}", path.display()),
);
} }
#[command] #[command]
@@ -55,346 +49,26 @@ pub fn open_link(app: AppHandle, url: String) {
api::shell::open(&app.shell_scope(), url, None).unwrap(); api::shell::open(&app.shell_scope(), url, None).unwrap();
} }
#[command]
pub fn get_chat_conf() -> ChatConfJson {
ChatConfJson::get_chat_conf()
}
#[command]
pub fn get_theme() -> String {
ChatConfJson::theme().unwrap_or(Theme::Light).to_string()
}
#[command]
pub fn reset_chat_conf() -> ChatConfJson {
ChatConfJson::reset_chat_conf()
}
#[command] #[command]
pub fn run_check_update(app: AppHandle, silent: bool, has_msg: Option<bool>) { pub fn run_check_update(app: AppHandle, silent: bool, has_msg: Option<bool>) {
utils::run_check_update(app, silent, has_msg); utils::run_check_update(app, silent, has_msg);
} }
#[command]
pub fn form_confirm(_app: AppHandle, data: serde_json::Value) {
ChatConfJson::amend(&serde_json::json!(data), None).unwrap();
}
#[command]
pub fn form_cancel(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label).unwrap();
tauri::api::dialog::ask(
app.app_handle().get_window(label).as_ref(),
title,
msg,
move |is_cancel| {
if is_cancel {
win.close().unwrap();
}
},
);
}
#[command]
pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label);
tauri::api::dialog::message(win.as_ref(), title, msg);
}
#[command] #[command]
pub fn open_file(path: PathBuf) { pub fn open_file(path: PathBuf) {
utils::open_file(path); utils::open_file(path);
} }
#[command] #[command]
pub fn get_chat_model_cmd() -> serde_json::Value { pub async fn get_data(app: AppHandle, url: String, is_msg: Option<bool>) -> Option<String> {
let path = utils::chat_root().join("chat.model.cmd.json"); let is_msg = is_msg.unwrap_or(false);
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string()); let res = if is_msg {
serde_json::from_str(&content).unwrap() utils::get_data(&url, Some(&app)).await
} } else {
utils::get_data(&url, None).await
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PromptRecord {
pub cmd: Option<String>,
pub act: String,
pub prompt: String,
}
#[command]
pub fn parse_prompt(data: String) -> Vec<PromptRecord> {
let mut rdr = csv::Reader::from_reader(data.as_bytes());
let mut list = vec![];
for result in rdr.deserialize() {
let record: PromptRecord = result.unwrap_or_else(|err| {
info!("parse_prompt_error: {}", err);
PromptRecord {
cmd: None,
act: "".to_string(),
prompt: "".to_string(),
}
});
if !record.act.is_empty() {
list.push(record);
}
}
list
}
#[command]
pub fn window_reload(app: AppHandle, label: &str) {
app.app_handle()
.get_window(label)
.unwrap()
.eval("window.location.reload()")
.unwrap();
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ModelRecord {
pub cmd: String,
pub act: String,
pub prompt: String,
pub tags: Vec<String>,
pub enable: bool,
}
#[command]
pub fn cmd_list() -> Vec<ModelRecord> {
let mut list = vec![];
for entry in WalkDir::new(utils::chat_root().join("cache_model"))
.into_iter()
.filter_map(|e| e.ok())
{
let file = fs::read_to_string(entry.path().display().to_string());
if let Ok(v) = file {
let data: Vec<ModelRecord> = serde_json::from_str(&v).unwrap_or_else(|_| vec![]);
let enable_list = data.into_iter().filter(|v| v.enable);
list.extend(enable_list)
}
}
// dbg!(&list);
list.sort_by(|a, b| a.cmd.len().cmp(&b.cmd.len()));
list
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct FileMetadata {
pub name: String,
pub ext: String,
pub created: u64,
pub id: String,
}
#[tauri::command]
pub fn get_download_list(pathname: &str) -> (Vec<serde_json::Value>, PathBuf) {
info!("get_download_list: {}", pathname);
let download_path = chat_root().join(PathBuf::from(pathname));
let content = fs::read_to_string(&download_path).unwrap_or_else(|err| {
info!("download_list_error: {}", err);
fs::write(&download_path, "[]").unwrap();
"[]".to_string()
});
let list = serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_else(|err| {
info!("download_list_parse_error: {}", err);
vec![]
});
(list, download_path)
}
#[command]
pub fn download_list(pathname: &str, dir: &str, filename: Option<String>, id: Option<String>) {
info!("download_list: {}", pathname);
let data = get_download_list(pathname);
let mut list = vec![];
let mut idmap = HashMap::new();
utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap);
for entry in WalkDir::new(utils::chat_root().join(dir))
.into_iter()
.filter_entry(|e| !utils::is_hidden(e))
.filter_map(|e| e.ok())
{
let metadata = entry.metadata().unwrap();
if metadata.is_file() {
let file_path = entry.path().display().to_string();
let re = Regex::new(r"(?P<id>[\d\w]+).(?P<ext>\w+)$").unwrap();
let caps = re.captures(&file_path).unwrap();
let fid = &caps["id"];
let fext = &caps["ext"];
let mut file_data = FileMetadata {
name: fid.to_string(),
id: fid.to_string(),
ext: fext.to_string(),
created: fs_extra::system_time_to_ms(metadata.created()),
}; };
res.unwrap_or_else(|err| {
if idmap.get(fid).is_some() { error!("chatgpt_client_http: {}", err);
let name = idmap.get(fid).unwrap().get("name").unwrap().clone();
match name {
serde_json::Value::String(v) => {
file_data.name = v.clone();
v
}
_ => "".to_string(),
};
}
if filename.is_some() && id.is_some() {
if let Some(ref v) = id {
if fid == v {
if let Some(ref v2) = filename {
file_data.name = v2.to_string();
}
}
}
}
list.push(serde_json::to_value(file_data).unwrap());
}
}
// dbg!(&list);
list.sort_by(|a, b| {
let a1 = a.get("created").unwrap().as_u64().unwrap();
let b1 = b.get("created").unwrap().as_u64().unwrap();
a1.cmp(&b1).reverse()
});
fs::write(data.1, serde_json::to_string_pretty(&list).unwrap()).unwrap();
}
#[command]
pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<ModelRecord>> {
let res = utils::get_data(GITHUB_PROMPTS_CSV_URL, Some(&app))
.await
.unwrap();
if let Some(v) = res {
let data = parse_prompt(v)
.iter()
.map(move |i| ModelRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
} else {
utils::gen_cmd(i.act.clone())
},
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["chatgpt-prompts".to_string()],
enable: true,
})
.collect::<Vec<ModelRecord>>();
let data2 = data.clone();
let model = utils::chat_root().join("chat.model.json");
let model_cmd = utils::chat_root().join("chat.model.cmd.json");
let chatgpt_prompts = utils::chat_root()
.join("cache_model")
.join("chatgpt_prompts.json");
if !utils::exists(&model) {
fs::write(
&model,
serde_json::json!({
"name": "ChatGPT Model",
"link": "https://github.com/lencx/ChatGPT"
})
.to_string(),
)
.unwrap();
}
// chatgpt_prompts.json
fs::write(
chatgpt_prompts,
serde_json::to_string_pretty(&data).unwrap(),
)
.unwrap();
let cmd_data = cmd_list();
// chat.model.cmd.json
fs::write(
model_cmd,
serde_json::to_string_pretty(&serde_json::json!({
"name": "ChatGPT CMD",
"last_updated": time,
"data": cmd_data,
}))
.unwrap(),
)
.unwrap();
let mut kv = HashMap::new();
kv.insert(
"sync_prompts".to_string(),
serde_json::json!({ "id": "chatgpt_prompts", "last_updated": time }),
);
let model_data = utils::merge(
&serde_json::from_str(&fs::read_to_string(&model).unwrap()).unwrap(),
&kv,
);
// chat.model.json
fs::write(model, serde_json::to_string_pretty(&model_data).unwrap()).unwrap();
// refresh window
api::dialog::message(
app.get_window("core").as_ref(),
"Sync Prompts",
"ChatGPT Prompts data has been synchronized!",
);
window_reload(app.clone(), "core");
window_reload(app, "tray");
return Some(data2);
}
None None
}
#[command]
pub async fn sync_user_prompts(url: String, data_type: String) -> Option<Vec<ModelRecord>> {
let res = utils::get_data(&url, None).await.unwrap_or_else(|err| {
info!("chatgpt_http_error: {}", err);
None
});
info!("chatgpt_http_url: {}", url);
if let Some(v) = res {
let data;
if data_type == "csv" {
info!("chatgpt_http_csv_parse");
data = parse_prompt(v);
} else if data_type == "json" {
info!("chatgpt_http_json_parse");
data = serde_json::from_str(&v).unwrap_or_else(|err| {
info!("chatgpt_http_json_parse_error: {}", err);
vec![]
});
} else {
info!("chatgpt_http_unknown_type");
data = vec![];
}
let data = data
.iter()
.map(move |i| ModelRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
} else {
utils::gen_cmd(i.act.clone())
},
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["user-sync".to_string()],
enable: true,
}) })
.collect::<Vec<ModelRecord>>();
return Some(data);
}
None
} }

View File

@@ -75,7 +75,8 @@ pub struct Metadata {
} }
pub fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 { pub fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
time.map(|t| { time
.map(|t| {
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap(); let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
duration_since_epoch.as_millis() as u64 duration_since_epoch.as_millis() as u64
}) })

293
src-tauri/src/app/gpt.rs Normal file
View File

@@ -0,0 +1,293 @@
use crate::{
app::{fs_extra, window},
conf::GITHUB_PROMPTS_CSV_URL,
utils,
};
use log::{error, info};
use regex::Regex;
use std::{collections::HashMap, fs, path::PathBuf, vec};
use tauri::{api, command, AppHandle, Manager};
use walkdir::WalkDir;
#[command]
pub fn get_chat_model_cmd() -> serde_json::Value {
let path = utils::app_root().join("chat.model.cmd.json");
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string());
serde_json::from_str(&content).unwrap()
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PromptRecord {
pub cmd: Option<String>,
pub act: String,
pub prompt: String,
}
#[command]
pub fn parse_prompt(data: String) -> Vec<PromptRecord> {
let mut rdr = csv::Reader::from_reader(data.as_bytes());
let mut list = vec![];
for result in rdr.deserialize() {
let record: PromptRecord = result.unwrap_or_else(|err| {
error!("parse_prompt: {}", err);
PromptRecord {
cmd: None,
act: "".to_string(),
prompt: "".to_string(),
}
});
if !record.act.is_empty() {
list.push(record);
}
}
list
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ModelRecord {
pub cmd: String,
pub act: String,
pub prompt: String,
pub tags: Vec<String>,
pub enable: bool,
}
#[command]
pub fn cmd_list() -> Vec<ModelRecord> {
let mut list = vec![];
for entry in WalkDir::new(utils::app_root().join("cache_model"))
.into_iter()
.filter_map(|e| e.ok())
{
let file = fs::read_to_string(entry.path().display().to_string());
if let Ok(v) = file {
let data: Vec<ModelRecord> = serde_json::from_str(&v).unwrap_or_else(|_| vec![]);
let enable_list = data.into_iter().filter(|v| v.enable);
list.extend(enable_list)
}
}
// dbg!(&list);
list.sort_by(|a, b| a.cmd.len().cmp(&b.cmd.len()));
list
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct FileMetadata {
pub name: String,
pub ext: String,
pub created: u64,
pub id: String,
}
#[tauri::command]
pub fn get_download_list(pathname: &str) -> (Vec<serde_json::Value>, PathBuf) {
info!("get_download_list: {}", pathname);
let download_path = utils::app_root().join(PathBuf::from(pathname));
let content = fs::read_to_string(&download_path).unwrap_or_else(|err| {
error!("download_list: {}", err);
fs::write(&download_path, "[]").unwrap();
"[]".to_string()
});
let list = serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_else(|err| {
error!("download_list_parse: {}", err);
vec![]
});
(list, download_path)
}
#[command]
pub fn download_list(pathname: &str, dir: &str, filename: Option<String>, id: Option<String>) {
info!("download_list: {}", pathname);
let data = get_download_list(pathname);
let mut list = vec![];
let mut idmap = HashMap::new();
utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap);
for entry in WalkDir::new(utils::app_root().join(dir))
.into_iter()
.filter_entry(|e| !utils::is_hidden(e))
.filter_map(|e| e.ok())
{
let metadata = entry.metadata().unwrap();
if metadata.is_file() {
let file_path = entry.path().display().to_string();
let re = Regex::new(r"(?P<id>[\d\w]+).(?P<ext>\w+)$").unwrap();
let caps = re.captures(&file_path).unwrap();
let fid = &caps["id"];
let fext = &caps["ext"];
let mut file_data = FileMetadata {
name: fid.to_string(),
id: fid.to_string(),
ext: fext.to_string(),
created: fs_extra::system_time_to_ms(metadata.created()),
};
if idmap.get(fid).is_some() {
let name = idmap.get(fid).unwrap().get("name").unwrap().clone();
match name {
serde_json::Value::String(v) => {
file_data.name = v.clone();
v
}
_ => "".to_string(),
};
}
if filename.is_some() && id.is_some() {
if let Some(ref v) = id {
if fid == v {
if let Some(ref v2) = filename {
file_data.name = v2.to_string();
}
}
}
}
list.push(serde_json::to_value(file_data).unwrap());
}
}
// dbg!(&list);
list.sort_by(|a, b| {
let a1 = a.get("created").unwrap().as_u64().unwrap();
let b1 = b.get("created").unwrap().as_u64().unwrap();
a1.cmp(&b1).reverse()
});
fs::write(data.1, serde_json::to_string_pretty(&list).unwrap()).unwrap();
}
#[command]
pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<ModelRecord>> {
let res = utils::get_data(GITHUB_PROMPTS_CSV_URL, Some(&app))
.await
.unwrap();
if let Some(v) = res {
let data = parse_prompt(v)
.iter()
.map(move |i| ModelRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
} else {
utils::gen_cmd(i.act.clone())
},
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["chatgpt-prompts".to_string()],
enable: true,
})
.collect::<Vec<ModelRecord>>();
let data2 = data.clone();
let model = utils::app_root().join("chat.model.json");
let model_cmd = utils::app_root().join("chat.model.cmd.json");
let chatgpt_prompts = utils::app_root()
.join("cache_model")
.join("chatgpt_prompts.json");
if !utils::exists(&model) {
fs::write(
&model,
serde_json::json!({
"name": "ChatGPT Model",
"link": "https://github.com/lencx/ChatGPT"
})
.to_string(),
)
.unwrap();
}
// chatgpt_prompts.json
fs::write(
chatgpt_prompts,
serde_json::to_string_pretty(&data).unwrap(),
)
.unwrap();
let cmd_data = cmd_list();
// chat.model.cmd.json
fs::write(
model_cmd,
serde_json::to_string_pretty(&serde_json::json!({
"name": "ChatGPT CMD",
"last_updated": time,
"data": cmd_data,
}))
.unwrap(),
)
.unwrap();
let mut kv = HashMap::new();
kv.insert(
"sync_prompts".to_string(),
serde_json::json!({ "id": "chatgpt_prompts", "last_updated": time }),
);
let model_data = utils::merge(
&serde_json::from_str(&fs::read_to_string(&model).unwrap()).unwrap(),
&kv,
);
// chat.model.json
fs::write(model, serde_json::to_string_pretty(&model_data).unwrap()).unwrap();
// refresh window
api::dialog::message(
app.get_window("core").as_ref(),
"Sync Prompts",
"ChatGPT Prompts data has been synchronized!",
);
window::cmd::window_reload(app.clone(), "core");
window::cmd::window_reload(app, "tray");
return Some(data2);
}
None
}
#[command]
pub async fn sync_user_prompts(url: String, data_type: String) -> Option<Vec<ModelRecord>> {
info!("sync_user_prompts: url => {}", url);
let res = utils::get_data(&url, None).await.unwrap_or_else(|err| {
error!("chatgpt_http: {}", err);
None
});
if let Some(v) = res {
let data;
if data_type == "csv" {
info!("chatgpt_http_csv_parse");
data = parse_prompt(v);
} else if data_type == "json" {
info!("chatgpt_http_json_parse");
data = serde_json::from_str(&v).unwrap_or_else(|err| {
error!("chatgpt_http_json_parse: {}", err);
vec![]
});
} else {
error!("chatgpt_http_unknown_type");
data = vec![];
}
let data = data
.iter()
.map(move |i| ModelRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
} else {
utils::gen_cmd(i.act.clone())
},
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["user-sync".to_string()],
enable: true,
})
.collect::<Vec<ModelRecord>>();
return Some(data);
}
None
}

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
app::{cmd, window}, app::window,
conf::{self, ChatConfJson}, conf::{self, AppConf},
utils, utils,
}; };
use tauri::{ use tauri::{
@@ -14,7 +14,7 @@ use tauri::AboutMetadata;
// --- Menu // --- Menu
pub fn init() -> Menu { pub fn init() -> Menu {
let chat_conf = ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
let name = "ChatGPT"; let name = "ChatGPT";
let app_menu = Submenu::new( let app_menu = Submenu::new(
name, name,
@@ -35,7 +35,7 @@ pub fn init() -> Menu {
let stay_on_top = let stay_on_top =
CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T"); CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T");
let stay_on_top_menu = if chat_conf.stay_on_top { let stay_on_top_menu = if app_conf.stay_on_top {
stay_on_top.selected() stay_on_top.selected()
} else { } else {
stay_on_top stay_on_top
@@ -44,37 +44,37 @@ pub fn init() -> Menu {
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light"); let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark"); let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
let theme_system = CustomMenuItem::new("theme_system".to_string(), "System"); let theme_system = CustomMenuItem::new("theme_system".to_string(), "System");
let is_dark = chat_conf.theme == "Dark"; let is_dark = app_conf.clone().theme_check("dark");
let is_system = chat_conf.theme == "System"; let is_system = app_conf.clone().theme_check("system");
let update_prompt = CustomMenuItem::new("update_prompt".to_string(), "Prompt"); let update_prompt = CustomMenuItem::new("update_prompt".to_string(), "Prompt");
let update_silent = CustomMenuItem::new("update_silent".to_string(), "Silent"); let update_silent = CustomMenuItem::new("update_silent".to_string(), "Silent");
let _update_disable = CustomMenuItem::new("update_disable".to_string(), "Disable"); // let _update_disable = CustomMenuItem::new("update_disable".to_string(), "Disable");
let popup_search = CustomMenuItem::new("popup_search".to_string(), "Pop-up Search"); let popup_search = CustomMenuItem::new("popup_search".to_string(), "Pop-up Search");
let popup_search_menu = if chat_conf.popup_search { let popup_search_menu = if app_conf.popup_search {
popup_search.selected() popup_search.selected()
} else { } else {
popup_search popup_search
}; };
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let titlebar = let titlebar = CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let titlebar_menu = if chat_conf.titlebar { let titlebar_menu = if app_conf.titlebar {
titlebar.selected() titlebar.selected()
} else { } else {
titlebar titlebar
}; };
let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray"); let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray");
let system_tray_menu = if chat_conf.tray { let system_tray_menu = if app_conf.tray {
system_tray.selected() system_tray.selected()
} else { } else {
system_tray system_tray
}; };
let auto_update = app_conf.get_auto_update();
let preferences_menu = Submenu::new( let preferences_menu = Submenu::new(
"Preferences", "Preferences",
Menu::with_items([ Menu::with_items([
@@ -115,16 +115,16 @@ pub fn init() -> Menu {
Submenu::new( Submenu::new(
"Auto Update", "Auto Update",
Menu::new() Menu::new()
.add_item(if chat_conf.auto_update == "Prompt" { .add_item(if auto_update == "prompt" {
update_prompt.selected() update_prompt.selected()
} else { } else {
update_prompt update_prompt
}) })
.add_item(if chat_conf.auto_update == "Silent" { .add_item(if auto_update == "silent" {
update_silent.selected() update_silent.selected()
} else { } else {
update_silent update_silent
}), // .add_item(if chat_conf.auto_update == "Disable" { }), // .add_item(if auto_update == "disable" {
// update_disable.selected() // update_disable.selected()
// } else { // } else {
// update_disable // update_disable
@@ -138,16 +138,11 @@ pub fn init() -> Menu {
CustomMenuItem::new("go_conf".to_string(), "Go to Config") CustomMenuItem::new("go_conf".to_string(), "Go to Config")
.accelerator("CmdOrCtrl+Shift+G") .accelerator("CmdOrCtrl+Shift+G")
.into(), .into(),
CustomMenuItem::new("clear_conf".to_string(), "Clear Config")
.accelerator("CmdOrCtrl+Shift+D")
.into(),
CustomMenuItem::new("restart".to_string(), "Restart ChatGPT") CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
.accelerator("CmdOrCtrl+Shift+R") .accelerator("CmdOrCtrl+Shift+R")
.into(), .into(),
CustomMenuItem::new("clear_conf".to_string(), "Clear Config").into(),
MenuItem::Separator.into(), MenuItem::Separator.into(),
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
.accelerator("CmdOrCtrl+Shift+A")
.into(),
CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(), CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(),
]), ]),
); );
@@ -167,12 +162,9 @@ pub fn init() -> Menu {
let view_menu = Submenu::new( let view_menu = Submenu::new(
"View", "View",
Menu::new() Menu::new()
.add_item(CustomMenuItem::new("go_back".to_string(), "Go Back").accelerator("CmdOrCtrl+["))
.add_item( .add_item(
CustomMenuItem::new("go_back".to_string(), "Go Back").accelerator("CmdOrCtrl+Left"), CustomMenuItem::new("go_forward".to_string(), "Go Forward").accelerator("CmdOrCtrl+]"),
)
.add_item(
CustomMenuItem::new("go_forward".to_string(), "Go Forward")
.accelerator("CmdOrCtrl+Right"),
) )
.add_item( .add_item(
CustomMenuItem::new("scroll_top".to_string(), "Scroll to Top of Screen") CustomMenuItem::new("scroll_top".to_string(), "Scroll to Top of Screen")
@@ -184,14 +176,23 @@ pub fn init() -> Menu {
) )
.add_native_item(MenuItem::Separator) .add_native_item(MenuItem::Separator)
.add_item( .add_item(
CustomMenuItem::new("reload".to_string(), "Refresh the Screen") CustomMenuItem::new("zoom_0".to_string(), "Zoom to Actual Size").accelerator("CmdOrCtrl+0"),
.accelerator("CmdOrCtrl+R"), )
.add_item(CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"))
.add_item(CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus"))
.add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("reload".to_string(), "Refresh the Screen").accelerator("CmdOrCtrl+R"),
), ),
); );
let window_menu = Submenu::new( let window_menu = Submenu::new(
"Window", "Window",
Menu::new() Menu::new()
.add_item(CustomMenuItem::new(
"app_website".to_string(),
"ChatGPT User's Guide",
))
.add_item(CustomMenuItem::new("dalle2".to_string(), "DALL·E 2")) .add_item(CustomMenuItem::new("dalle2".to_string(), "DALL·E 2"))
.add_native_item(MenuItem::Separator) .add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Minimize) .add_native_item(MenuItem::Minimize)
@@ -244,24 +245,31 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
utils::run_check_update(app, false, None); utils::run_check_update(app, false, None);
} }
// Preferences // Preferences
"control_center" => window::control_window(&app), "control_center" => window::cmd::control_window(app),
"restart" => tauri::api::process::restart(&app.env()), "restart" => tauri::api::process::restart(&app.env()),
"inject_script" => open(&app, script_path), "inject_script" => open(&app, script_path),
"go_conf" => utils::open_file(utils::chat_root()), "go_conf" => utils::open_file(utils::app_root()),
"clear_conf" => utils::clear_conf(&app), "clear_conf" => utils::clear_conf(&app),
"awesome" => open(&app, conf::AWESOME_URL.to_string()), "app_website" => window::cmd::wa_window(
app,
"app_website".into(),
"ChatGPT User's Guide".into(),
conf::APP_WEBSITE.into(),
None,
),
"buy_coffee" => open(&app, conf::BUY_COFFEE.to_string()), "buy_coffee" => open(&app, conf::BUY_COFFEE.to_string()),
"popup_search" => { "popup_search" => {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
let popup_search = !chat_conf.popup_search; let popup_search = !app_conf.popup_search;
menu_handle menu_handle
.get_item(menu_id) .get_item(menu_id)
.set_selected(popup_search) .set_selected(popup_search)
.unwrap(); .unwrap();
ChatConfJson::amend(&serde_json::json!({ "popup_search": popup_search }), None) app_conf
.unwrap(); .amend(serde_json::json!({ "popup_search": popup_search }))
cmd::window_reload(app.clone(), "core"); .write();
cmd::window_reload(app, "tray"); window::cmd::window_reload(app.clone(), "core");
window::cmd::window_reload(app, "tray");
} }
"sync_prompts" => { "sync_prompts" => {
tauri::api::dialog::ask( tauri::api::dialog::ask(
@@ -270,7 +278,8 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
"Data sync will enable all prompts, are you sure you want to sync?", "Data sync will enable all prompts, are you sure you want to sync?",
move |is_restart| { move |is_restart| {
if is_restart { if is_restart {
app.get_window("core") app
.get_window("core")
.unwrap() .unwrap()
.eval("window.__sync_prompts && window.__sync_prompts()") .eval("window.__sync_prompts && window.__sync_prompts()")
.unwrap() .unwrap()
@@ -279,29 +288,37 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
); );
} }
"hide_dock_icon" => { "hide_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap() AppConf::read()
.amend(serde_json::json!({ "hide_dock_icon": true }))
.write()
.restart(app);
} }
"titlebar" => { "titlebar" => {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
ChatConfJson::amend( app_conf
&serde_json::json!({ "titlebar": !chat_conf.titlebar }), .clone()
None, .amend(serde_json::json!({ "titlebar": !app_conf.titlebar }))
) .write()
.unwrap(); .restart(app);
tauri::api::process::restart(&app.env());
} }
"system_tray" => { "system_tray" => {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
ChatConfJson::amend(&serde_json::json!({ "tray": !chat_conf.tray }), None).unwrap(); app_conf
tauri::api::process::restart(&app.env()); .clone()
.amend(serde_json::json!({ "tray": !app_conf.tray }))
.write()
.restart(app);
} }
"theme_light" | "theme_dark" | "theme_system" => { "theme_light" | "theme_dark" | "theme_system" => {
let theme = match menu_id { let theme = match menu_id {
"theme_dark" => "Dark", "theme_dark" => "dark",
"theme_system" => "System", "theme_system" => "system",
_ => "Light", _ => "light",
}; };
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap(); AppConf::read()
.amend(serde_json::json!({ "theme": theme }))
.write()
.restart(app);
} }
"update_prompt" | "update_silent" | "update_disable" => { "update_prompt" | "update_silent" | "update_disable" => {
// for id in ["update_prompt", "update_silent", "update_disable"] { // for id in ["update_prompt", "update_silent", "update_disable"] {
@@ -314,42 +331,48 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
.get_item("update_silent") .get_item("update_silent")
.set_selected(true) .set_selected(true)
.unwrap(); .unwrap();
"Silent" "silent"
} }
"update_disable" => { "update_disable" => {
menu_handle menu_handle
.get_item("update_disable") .get_item("update_disable")
.set_selected(true) .set_selected(true)
.unwrap(); .unwrap();
"Disable" "disable"
} }
_ => { _ => {
menu_handle menu_handle
.get_item("update_prompt") .get_item("update_prompt")
.set_selected(true) .set_selected(true)
.unwrap(); .unwrap();
"Prompt" "prompt"
} }
}; };
ChatConfJson::amend(&serde_json::json!({ "auto_update": auto_update }), None).unwrap(); AppConf::read()
.amend(serde_json::json!({ "auto_update": auto_update }))
.write();
} }
"stay_on_top" => { "stay_on_top" => {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
let stay_on_top = !chat_conf.stay_on_top; let stay_on_top = !app_conf.stay_on_top;
menu_handle menu_handle
.get_item(menu_id) .get_item(menu_id)
.set_selected(stay_on_top) .set_selected(stay_on_top)
.unwrap(); .unwrap();
win.set_always_on_top(stay_on_top).unwrap(); win.set_always_on_top(stay_on_top).unwrap();
ChatConfJson::amend(&serde_json::json!({ "stay_on_top": stay_on_top }), None).unwrap(); app_conf
.amend(serde_json::json!({ "stay_on_top": stay_on_top }))
.write();
} }
// Window // Window
"dalle2" => window::dalle2_window(&app, None, None, Some(false)), "dalle2" => window::dalle2_window(&app, None, None, Some(false)),
// View // View
"zoom_0" => win.eval("window.__zoom0 && window.__zoom0()").unwrap(),
"zoom_out" => win.eval("window.__zoomOut && window.__zoomOut()").unwrap(),
"zoom_in" => win.eval("window.__zoomIn && window.__zoomIn()").unwrap(),
"reload" => win.eval("window.location.reload()").unwrap(), "reload" => win.eval("window.location.reload()").unwrap(),
"go_back" => win.eval("window.history.go(-1)").unwrap(), "go_back" => win.eval("window.history.go(-1)").unwrap(),
"go_forward" => win.eval("window.history.go(1)").unwrap(), "go_forward" => win.eval("window.history.go(1)").unwrap(),
// core: document.querySelector('main .overflow-y-auto')
"scroll_top" => win "scroll_top" => win
.eval( .eval(
r#"window.scroll({ r#"window.scroll({
@@ -368,7 +391,7 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
) )
.unwrap(), .unwrap(),
// Help // Help
"chatgpt_log" => utils::open_file(utils::chat_root().join("chatgpt.log")), "chatgpt_log" => utils::open_file(utils::app_root().join("chatgpt.log")),
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()), "update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
"report_bug" => open(&app, conf::ISSUES_URL.to_string()), "report_bug" => open(&app, conf::ISSUES_URL.to_string()),
"dev_tools" => { "dev_tools" => {
@@ -382,22 +405,29 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
// --- SystemTray Menu // --- SystemTray Menu
pub fn tray_menu() -> SystemTray { pub fn tray_menu() -> SystemTray {
if cfg!(target_os = "macos") { if cfg!(target_os = "macos") {
SystemTray::new().with_menu( let mut tray_menu = SystemTrayMenu::new()
SystemTrayMenu::new()
.add_item(CustomMenuItem::new( .add_item(CustomMenuItem::new(
"control_center".to_string(), "control_center".to_string(),
"Control Center", "Control Center",
)) ))
.add_native_item(SystemTrayMenuItem::Separator) .add_native_item(SystemTrayMenuItem::Separator);
.add_item(CustomMenuItem::new(
if AppConf::read().hide_dock_icon {
tray_menu = tray_menu.add_item(CustomMenuItem::new(
"show_dock_icon".to_string(), "show_dock_icon".to_string(),
"Show Dock Icon", "Show Dock Icon",
)) ));
} else {
tray_menu = tray_menu
.add_item(CustomMenuItem::new( .add_item(CustomMenuItem::new(
"hide_dock_icon".to_string(), "hide_dock_icon".to_string(),
"Hide Dock Icon", "Hide Dock Icon",
)) ))
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT")) .add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"));
}
SystemTray::new().with_menu(
tray_menu
.add_native_item(SystemTrayMenuItem::Separator) .add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")), .add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
) )
@@ -423,14 +453,15 @@ pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
match event { match event {
SystemTrayEvent::LeftClick { .. } => { SystemTrayEvent::LeftClick { .. } => {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
if !chat_conf.hide_dock_icon { if !app_conf.hide_dock_icon {
let core_win = handle.get_window("core").unwrap(); if let Some(core_win) = handle.get_window("core") {
core_win.minimize().unwrap(); core_win.minimize().unwrap();
} }
}
let tray_win = handle.get_window("tray").unwrap(); if let Some(tray_win) = handle.get_window("tray") {
tray_win.move_window(Position::TrayCenter).unwrap(); tray_win.move_window(Position::TrayCenter).unwrap();
if tray_win.is_visible().unwrap() { if tray_win.is_visible().unwrap() {
@@ -439,28 +470,34 @@ pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
tray_win.show().unwrap(); tray_win.show().unwrap();
} }
} }
}
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"control_center" => window::control_window(&app), "control_center" => window::cmd::control_window(app),
"restart" => tauri::api::process::restart(&handle.env()), "restart" => tauri::api::process::restart(&handle.env()),
"show_dock_icon" => { "show_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app)) AppConf::read()
.unwrap(); .amend(serde_json::json!({ "hide_dock_icon": false }))
.write()
.restart(app);
} }
"hide_dock_icon" => { "hide_dock_icon" => {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
if !chat_conf.hide_dock_icon { if !app_conf.hide_dock_icon {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)) app_conf
.unwrap(); .amend(serde_json::json!({ "hide_dock_icon": true }))
.write()
.restart(app);
} }
} }
"show_core" => { "show_core" => {
let core_win = app.get_window("core").unwrap(); if let Some(core_win) = app.get_window("core") {
let tray_win = app.get_window("tray").unwrap(); let tray_win = app.get_window("tray").unwrap();
if !core_win.is_visible().unwrap() { if !core_win.is_visible().unwrap() {
core_win.show().unwrap(); core_win.show().unwrap();
core_win.set_focus().unwrap(); core_win.set_focus().unwrap();
tray_win.hide().unwrap(); tray_win.hide().unwrap();
} }
};
} }
"quit" => std::process::exit(0), "quit" => std::process::exit(0),
_ => (), _ => (),

View File

@@ -1,5 +1,6 @@
pub mod cmd; pub mod cmd;
pub mod fs_extra; pub mod fs_extra;
pub mod gpt;
pub mod menu; pub mod menu;
pub mod setup; pub mod setup;
pub mod window; pub mod window;

View File

@@ -1,20 +1,21 @@
use crate::{app::window, conf::ChatConfJson, utils}; use crate::{app::window, conf::AppConf, utils};
use log::info; use log::{error, info};
use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, GlobalShortcutManager, Manager}; use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, GlobalShortcutManager, Manager};
use wry::application::accelerator::Accelerator; use wry::application::accelerator::Accelerator;
pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>> { pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("stepup"); info!("stepup");
let chat_conf = ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
let url = chat_conf.origin.to_string(); let url = app_conf.main_origin.to_string();
let theme = ChatConfJson::theme(); let theme = AppConf::theme_mode();
let handle = app.app_handle(); let handle = app.app_handle();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
info!("stepup_tray");
window::tray_window(&handle); window::tray_window(&handle);
}); });
if let Some(v) = chat_conf.global_shortcut { if let Some(v) = app_conf.clone().global_shortcut {
info!("global_shortcut: `{}`", v); info!("global_shortcut: `{}`", v);
match v.parse::<Accelerator>() { match v.parse::<Accelerator>() {
Ok(_) => { Ok(_) => {
@@ -33,80 +34,72 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
} }
}) })
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
info!("global_shortcut_register_error: {}", err); error!("global_shortcut_register_error: {}", err);
}); });
} }
Err(err) => { Err(err) => {
info!("global_shortcut_parse_error: {}", err); error!("global_shortcut_parse_error: {}", err);
} }
} }
} else { } else {
info!("global_shortcut_unregister"); info!("global_shortcut_unregister");
}; };
if chat_conf.hide_dock_icon { let app_conf2 = app_conf.clone();
if app_conf.hide_dock_icon {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory); app.set_activation_policy(tauri::ActivationPolicy::Accessory);
} else { } else {
let app = app.handle(); let app = app.handle();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
#[cfg(target_os = "macos")] let link = if app_conf2.main_dashboard {
WindowBuilder::new(&app, "core", WindowUrl::App(url.into())) "index.html"
} else {
&url
};
info!("main_window: {}", link);
let mut main_win = WindowBuilder::new(&app, "core", WindowUrl::App(link.into()))
.title("ChatGPT") .title("ChatGPT")
.resizable(true) .resizable(true)
.fullscreen(false) .fullscreen(false)
.inner_size(800.0, 600.0) .inner_size(800.0, 600.0)
.hidden_title(true) .theme(Some(theme))
.theme(theme) .always_on_top(app_conf2.stay_on_top)
.always_on_top(chat_conf.stay_on_top)
.title_bar_style(ChatConfJson::titlebar())
.initialization_script(&utils::user_script()) .initialization_script(&utils::user_script())
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
.initialization_script(include_str!("../vendors/html2canvas.js"))
.initialization_script(include_str!("../vendors/jspdf.js"))
.initialization_script(include_str!("../vendors/turndown.js"))
.initialization_script(include_str!("../vendors/turndown-plugin-gfm.js"))
.initialization_script(include_str!("../scripts/core.js")) .initialization_script(include_str!("../scripts/core.js"))
.initialization_script(include_str!("../scripts/popup.core.js")) .user_agent(&app_conf2.ua_window);
.initialization_script(include_str!("../scripts/export.js"))
.initialization_script(include_str!("../scripts/markdown.export.js"))
.initialization_script(include_str!("../scripts/cmd.js"))
.user_agent(&chat_conf.ua_window)
.build()
.unwrap();
#[cfg(not(target_os = "macos"))] #[cfg(target_os = "macos")]
WindowBuilder::new(&app, "core", WindowUrl::App(url.into())) {
.title("ChatGPT") main_win = main_win
.resizable(true) .title_bar_style(app_conf2.clone().titlebar())
.fullscreen(false) .hidden_title(true);
.inner_size(800.0, 600.0) }
.theme(theme)
.always_on_top(chat_conf.stay_on_top) if url == "https://chat.openai.com" && !app_conf2.main_dashboard {
.initialization_script(&utils::user_script()) main_win = main_win
.initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js"))
.initialization_script(include_str!("../vendors/html2canvas.js")) .initialization_script(include_str!("../vendors/html2canvas.js"))
.initialization_script(include_str!("../vendors/jspdf.js")) .initialization_script(include_str!("../vendors/jspdf.js"))
.initialization_script(include_str!("../vendors/turndown.js")) .initialization_script(include_str!("../vendors/turndown.js"))
.initialization_script(include_str!("../vendors/turndown-plugin-gfm.js")) .initialization_script(include_str!("../vendors/turndown-plugin-gfm.js"))
.initialization_script(include_str!("../scripts/core.js"))
.initialization_script(include_str!("../scripts/popup.core.js")) .initialization_script(include_str!("../scripts/popup.core.js"))
.initialization_script(include_str!("../scripts/export.js")) .initialization_script(include_str!("../scripts/export.js"))
.initialization_script(include_str!("../scripts/markdown.export.js")) .initialization_script(include_str!("../scripts/markdown.export.js"))
.initialization_script(include_str!("../scripts/cmd.js")) .initialization_script(include_str!("../scripts/cmd.js"))
.user_agent(&chat_conf.ua_window) }
.build()
.unwrap(); main_win.build().unwrap();
}); });
} }
// auto_update // auto_update
if chat_conf.auto_update != "Disable" { let auto_update = app_conf.get_auto_update();
info!("stepup::run_check_update"); if auto_update != "disable" {
info!("run_check_update");
let app = app.handle(); let app = app.handle();
utils::run_check_update(app, chat_conf.auto_update == "Silent", None); utils::run_check_update(app, auto_update == "silent", None);
} }
Ok(()) Ok(())

View File

@@ -1,33 +1,40 @@
use crate::{conf, utils}; use crate::{conf::AppConf, utils};
use log::info; use log::info;
use std::time::SystemTime; use std::time::SystemTime;
use tauri::{utils::config::WindowUrl, window::WindowBuilder, Manager}; use tauri::{utils::config::WindowUrl, window::WindowBuilder, Manager};
pub fn tray_window(handle: &tauri::AppHandle) { pub fn tray_window(handle: &tauri::AppHandle) {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let app_conf = AppConf::read();
let theme = conf::ChatConfJson::theme(); let theme = AppConf::theme_mode();
let app = handle.clone(); let app = handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
WindowBuilder::new(&app, "tray", WindowUrl::App(chat_conf.origin.into())) let link = if app_conf.tray_dashboard {
"index.html"
} else {
&app_conf.tray_origin
};
let mut tray_win = WindowBuilder::new(&app, "tray", WindowUrl::App(link.into()))
.title("ChatGPT") .title("ChatGPT")
.resizable(false) .resizable(false)
.fullscreen(false) .fullscreen(false)
.inner_size(360.0, 540.0) .inner_size(360.0, 540.0)
.decorations(false) .decorations(false)
.always_on_top(true) .always_on_top(true)
.theme(theme) .theme(Some(theme))
.initialization_script(&utils::user_script()) .initialization_script(&utils::user_script())
.initialization_script(include_str!("../scripts/core.js"))
.user_agent(&app_conf.ua_tray);
if app_conf.tray_origin == "https://chat.openai.com" && !app_conf.tray_dashboard {
tray_win = tray_win
.initialization_script(include_str!("../vendors/floating-ui-core.js")) .initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js")) .initialization_script(include_str!("../vendors/floating-ui-dom.js"))
.initialization_script(include_str!("../scripts/core.js"))
.initialization_script(include_str!("../scripts/cmd.js")) .initialization_script(include_str!("../scripts/cmd.js"))
.initialization_script(include_str!("../scripts/popup.core.js")) .initialization_script(include_str!("../scripts/popup.core.js"))
.user_agent(&chat_conf.ua_tray) }
.build()
.unwrap() tray_win.build().unwrap().hide().unwrap();
.hide()
.unwrap();
}); });
} }
@@ -38,14 +45,11 @@ pub fn dalle2_window(
is_new: Option<bool>, is_new: Option<bool>,
) { ) {
info!("dalle2_query: {:?}", query); info!("dalle2_query: {:?}", query);
let theme = conf::ChatConfJson::theme(); let theme = AppConf::theme_mode();
let app = handle.clone(); let app = handle.clone();
let query = if query.is_some() { let query = if query.is_some() {
format!( format!("window.addEventListener('DOMContentLoaded', function() {{\nwindow.__CHATGPT_QUERY__='{}';\n}})", query.unwrap())
"window.addEventListener('DOMContentLoaded', function() {{\nwindow.__CHATGPT_QUERY__='{}';\n}})",
query.unwrap()
)
} else { } else {
"".to_string() "".to_string()
}; };
@@ -72,7 +76,7 @@ pub fn dalle2_window(
.fullscreen(false) .fullscreen(false)
.inner_size(800.0, 600.0) .inner_size(800.0, 600.0)
.always_on_top(false) .always_on_top(false)
.theme(theme) .theme(Some(theme))
.initialization_script(include_str!("../scripts/core.js")) .initialization_script(include_str!("../scripts/core.js"))
.initialization_script(&query) .initialization_script(&query)
.initialization_script(include_str!("../scripts/dalle2.js")) .initialization_script(include_str!("../scripts/dalle2.js"))
@@ -86,22 +90,83 @@ pub fn dalle2_window(
} }
} }
pub fn control_window(handle: &tauri::AppHandle) { pub mod cmd {
let app = handle.clone(); use super::*;
use log::info;
use tauri::{command, utils::config::WindowUrl, window::WindowBuilder, Manager};
#[tauri::command]
pub fn dalle2_search_window(app: tauri::AppHandle, query: String) {
dalle2_window(
&app.app_handle(),
Some(query),
Some("ChatGPT & DALL·E 2".to_string()),
None,
);
}
#[tauri::command]
pub fn control_window(handle: tauri::AppHandle) {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
if app.app_handle().get_window("main").is_none() { if handle.get_window("main").is_none() {
WindowBuilder::new(&app, "main", WindowUrl::App("index.html".into())) WindowBuilder::new(
&handle,
"main",
WindowUrl::App("index.html?type=control".into()),
)
.title("Control Center") .title("Control Center")
.resizable(true) .resizable(true)
.fullscreen(false) .fullscreen(false)
.inner_size(800.0, 600.0) .inner_size(1200.0, 700.0)
.min_inner_size(800.0, 600.0) .min_inner_size(1000.0, 600.0)
.build() .build()
.unwrap(); .unwrap();
} else { } else {
let main_win = app.app_handle().get_window("main").unwrap(); let main_win = handle.get_window("main").unwrap();
main_win.show().unwrap(); main_win.show().unwrap();
main_win.set_focus().unwrap(); main_win.set_focus().unwrap();
} }
}); });
} }
#[command]
pub fn wa_window(
app: tauri::AppHandle,
label: String,
title: String,
url: String,
script: Option<String>,
) {
info!("wa_window: {} :=> {}", title, url);
let win = app.get_window(&label);
if win.is_none() {
tauri::async_runtime::spawn(async move {
tauri::WindowBuilder::new(&app, label, tauri::WindowUrl::App(url.parse().unwrap()))
.initialization_script(&script.unwrap_or_default())
.initialization_script(include_str!("../scripts/core.js"))
.title(title)
.inner_size(960.0, 700.0)
.resizable(true)
.build()
.unwrap();
});
}
if let Some(v) = win {
if !v.is_visible().unwrap() {
v.show().unwrap();
}
v.eval("window.location.reload()").unwrap();
v.set_focus().unwrap();
}
}
#[command]
pub fn window_reload(app: tauri::AppHandle, label: &str) {
app
.app_handle()
.get_window(label)
.unwrap()
.eval("window.location.reload()")
.unwrap();
}
}

View File

@@ -1,179 +1,142 @@
use crate::utils::{chat_root, create_file, exists}; use log::{error, info};
use anyhow::Result;
use log::info;
use serde_json::Value; use serde_json::Value;
use std::{collections::BTreeMap, fs, path::PathBuf}; use std::{collections::BTreeMap, path::PathBuf};
use tauri::{Manager, Theme}; use tauri::{Manager, Theme};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
// pub const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15"; use crate::utils::{app_root, create_file, exists};
// pub const PHONE_USER_AGENT: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1";
pub const APP_WEBSITE: &str = "https://lencx.github.io/app/";
pub const ISSUES_URL: &str = "https://github.com/lencx/ChatGPT/issues"; pub const ISSUES_URL: &str = "https://github.com/lencx/ChatGPT/issues";
pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md"; pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md";
pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md";
pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx"; pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx";
pub const GITHUB_PROMPTS_CSV_URL: &str = pub const GITHUB_PROMPTS_CSV_URL: &str =
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv"; "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
pub const DEFAULT_CHAT_CONF: &str = r#"{
"stay_on_top": false,
"auto_update": "Prompt",
"theme": "Light",
"tray": true,
"titlebar": true,
"popup_search": false,
"global_shortcut": "",
"hide_dock_icon": false,
"default_origin": "https://chat.openai.com",
"origin": "https://chat.openai.com",
"ua_window": "",
"ua_tray": ""
}"#;
pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
"stay_on_top": false,
"auto_update": "Prompt",
"theme": "Light",
"tray": true,
"titlebar": false,
"popup_search": false,
"global_shortcut": "",
"hide_dock_icon": false,
"default_origin": "https://chat.openai.com",
"origin": "https://chat.openai.com",
"ua_window": "",
"ua_tray": ""
}"#;
pub const APP_CONF_PATH: &str = "chat.conf.json";
pub const CHATGPT_URL: &str = "https://chat.openai.com";
pub const UA_MOBILE: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1";
macro_rules! pub_struct {
($name:ident {$($field:ident: $t:ty,)*}) => {
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ChatConfJson { pub struct $name {
// support macOS only $(pub $field: $t),*
pub titlebar: bool, }
pub hide_dock_icon: bool, }
// macOS and Windows, Light/Dark/System
pub theme: String,
// auto update policy, Prompt/Silent/Disable
pub auto_update: String,
pub tray: bool,
pub popup_search: bool,
pub stay_on_top: bool,
pub default_origin: String,
pub origin: String,
pub ua_window: String,
pub ua_tray: String,
pub global_shortcut: Option<String>,
} }
impl ChatConfJson { pub_struct!(AppConf {
/// init chat.conf.json titlebar: bool,
/// path: ~/.chatgpt/chat.conf.json hide_dock_icon: bool,
pub fn init() -> PathBuf { // macOS and Windows: light / dark / system
info!("chat_conf_init"); theme: String,
let conf_file = ChatConfJson::conf_path(); // auto update policy: prompt / silent / disable
let content = if cfg!(target_os = "macos") { auto_update: String,
DEFAULT_CHAT_CONF_MAC tray: bool,
} else { popup_search: bool,
DEFAULT_CHAT_CONF stay_on_top: bool,
}; main_dashboard: bool,
tray_dashboard: bool,
main_origin: String,
tray_origin: String,
default_origin: String,
ua_window: String,
ua_tray: String,
global_shortcut: Option<String>,
});
if !exists(&conf_file) { impl AppConf {
create_file(&conf_file).unwrap(); pub fn new() -> Self {
fs::write(&conf_file, content).unwrap(); info!("conf_init");
return conf_file; Self {
titlebar: !cfg!(target_os = "macos"),
hide_dock_icon: false,
theme: "light".into(),
auto_update: "prompt".into(),
tray: true,
popup_search: false,
stay_on_top: false,
main_dashboard: false,
tray_dashboard: false,
main_origin: CHATGPT_URL.into(),
tray_origin: CHATGPT_URL.into(),
default_origin: CHATGPT_URL.into(),
ua_tray: UA_MOBILE.into(),
ua_window: "".into(),
global_shortcut: None,
}
} }
let conf_file = ChatConfJson::conf_path(); pub fn file_path() -> PathBuf {
let file_content = fs::read_to_string(&conf_file).unwrap(); app_root().join(APP_CONF_PATH)
match serde_json::from_str(&file_content) { }
Ok(v) => v,
pub fn read() -> Self {
match std::fs::read_to_string(Self::file_path()) {
Ok(v) => serde_json::from_str::<AppConf>(&v).unwrap(),
Err(err) => { Err(err) => {
if err.to_string() == "invalid type: map, expected unit at line 1 column 0" { error!("conf_read_error: {}", err);
return conf_file; Self::default()
} }
fs::write(&conf_file, content).unwrap();
} }
};
conf_file
} }
pub fn conf_path() -> PathBuf { pub fn write(self) -> Self {
chat_root().join("chat.conf.json") let path = &Self::file_path();
if !exists(path) {
create_file(path).unwrap();
info!("conf_create");
} }
if let Ok(v) = serde_json::to_string_pretty(&self) {
pub fn get_chat_conf() -> Self { std::fs::write(path, v).unwrap_or_else(|err| {
let conf_file = ChatConfJson::conf_path(); error!("conf_write: {}", err);
let file_content = fs::read_to_string(&conf_file).unwrap(); Self::default().write();
let content = if cfg!(target_os = "macos") { });
DEFAULT_CHAT_CONF_MAC
} else { } else {
DEFAULT_CHAT_CONF error!("conf_ser");
};
match serde_json::from_value(match serde_json::from_str(&file_content) {
Ok(v) => v,
Err(_) => {
fs::write(&conf_file, content).unwrap();
serde_json::from_str(content).unwrap()
}
}) {
Ok(v) => v,
Err(_) => {
fs::write(&conf_file, content).unwrap();
serde_json::from_value(serde_json::from_str(content).unwrap()).unwrap()
}
} }
self
} }
pub fn reset_chat_conf() -> Self { pub fn amend(self, json: Value) -> Self {
let conf_file = ChatConfJson::conf_path(); let val = serde_json::to_value(&self).unwrap();
let content = if cfg!(target_os = "macos") { let mut config: BTreeMap<String, Value> = serde_json::from_value(val).unwrap();
DEFAULT_CHAT_CONF_MAC let new_json: BTreeMap<String, Value> = serde_json::from_value(json).unwrap();
} else {
DEFAULT_CHAT_CONF
};
fs::write(&conf_file, content).unwrap();
serde_json::from_str(content).unwrap()
}
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3 for (k, v) in new_json {
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> {
let config = ChatConfJson::get_chat_conf();
let config: Value = serde_json::to_value(&config)?;
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
let new_rules: BTreeMap<String, Value> = serde_json::from_value(new_rules.clone())?;
for (k, v) in new_rules {
config.insert(k, v); config.insert(k, v);
} }
fs::write( match serde_json::to_string_pretty(&config) {
ChatConfJson::conf_path(), Ok(v) => match serde_json::from_str::<AppConf>(&v) {
serde_json::to_string_pretty(&config)?, Ok(v) => v,
)?; Err(err) => {
error!("conf_amend_parse: {}", err);
if let Some(handle) = app { self
tauri::api::process::restart(&handle.env()); }
// tauri::api::dialog::ask( },
// handle.get_window("core").as_ref(), Err(err) => {
// "ChatGPT Restart", error!("conf_amend_str: {}", err);
// "Whether to restart immediately?", self
// move |is_restart| { }
// if is_restart { }
// }
// },
// );
} }
Ok(()) #[cfg(target_os = "macos")]
pub fn titlebar(self) -> TitleBarStyle {
if self.titlebar {
TitleBarStyle::Transparent
} else {
TitleBarStyle::Overlay
}
} }
pub fn theme() -> Option<Theme> { pub fn theme_mode() -> Theme {
let conf = ChatConfJson::get_chat_conf(); match Self::get_theme().as_str() {
let theme = match conf.theme.as_str() { "system" => match dark_light::detect() {
"System" => match dark_light::detect() {
// Dark mode // Dark mode
dark_light::Mode::Dark => Theme::Dark, dark_light::Mode::Dark => Theme::Dark,
// Light mode // Light mode
@@ -181,20 +144,76 @@ impl ChatConfJson {
// Unspecified // Unspecified
dark_light::Mode::Default => Theme::Light, dark_light::Mode::Default => Theme::Light,
}, },
"Dark" => Theme::Dark, "dark" => Theme::Dark,
_ => Theme::Light, _ => Theme::Light,
}; }
Some(theme)
} }
#[cfg(target_os = "macos")] pub fn get_theme() -> String {
pub fn titlebar() -> TitleBarStyle { Self::read().theme.to_lowercase()
let conf = ChatConfJson::get_chat_conf(); }
if conf.titlebar {
TitleBarStyle::Transparent pub fn get_auto_update(self) -> String {
} else { self.auto_update.to_lowercase()
TitleBarStyle::Overlay }
pub fn theme_check(self, mode: &str) -> bool {
self.theme.to_lowercase() == mode
}
pub fn restart(self, app: tauri::AppHandle) {
tauri::api::process::restart(&app.env());
} }
} }
impl Default for AppConf {
fn default() -> Self {
Self::new()
}
}
pub mod cmd {
use super::AppConf;
use tauri::{command, AppHandle, Manager};
#[command]
pub fn get_app_conf() -> AppConf {
AppConf::read()
}
#[command]
pub fn reset_app_conf() -> AppConf {
AppConf::default().write()
}
#[command]
pub fn get_theme() -> String {
AppConf::get_theme()
}
#[command]
pub fn form_confirm(_app: AppHandle, data: serde_json::Value) {
AppConf::read().amend(serde_json::json!(data)).write();
}
#[command]
pub fn form_cancel(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label).unwrap();
tauri::api::dialog::ask(
app.app_handle().get_window(label).as_ref(),
title,
msg,
move |is_cancel| {
if is_cancel {
win.close().unwrap();
}
},
);
}
#[command]
pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label);
tauri::api::dialog::message(win.as_ref(), title, msg);
}
} }

View File

@@ -7,49 +7,46 @@ mod app;
mod conf; mod conf;
mod utils; mod utils;
use app::{cmd, fs_extra, menu, setup}; use app::{cmd, fs_extra, gpt, menu, setup, window};
use conf::ChatConfJson; use conf::AppConf;
use tauri::api::path;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_log::{ use tauri_plugin_log::{
fern::colors::{Color, ColoredLevelConfig}, fern::colors::{Color, ColoredLevelConfig},
LogTarget, LoggerBuilder, LogTarget,
}; };
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
ChatConfJson::init(); let app_conf = AppConf::read().write();
// If the file does not exist, creating the file will block menu synchronization // If the file does not exist, creating the file will block menu synchronization
utils::create_chatgpt_prompts(); utils::create_chatgpt_prompts();
let context = tauri::generate_context!(); let context = tauri::generate_context!();
let colors = ColoredLevelConfig {
gpt::download_list("chat.download.json", "download", None, None);
gpt::download_list("chat.notes.json", "notes", None, None);
let mut log = tauri_plugin_log::Builder::default()
.targets([
// LogTarget::LogDir,
// LOG PATH: ~/.chatgpt/ChatGPT.log
LogTarget::Folder(utils::app_root()),
LogTarget::Stdout,
LogTarget::Webview,
])
.level(log::LevelFilter::Debug);
if cfg!(debug_assertions) {
log = log.with_colors(ColoredLevelConfig {
error: Color::Red, error: Color::Red,
warn: Color::Yellow, warn: Color::Yellow,
debug: Color::Blue, debug: Color::Blue,
info: Color::BrightGreen, info: Color::BrightGreen,
trace: Color::Cyan, trace: Color::Cyan,
}; });
}
cmd::download_list("chat.download.json", "download", None, None);
cmd::download_list("chat.notes.json", "notes", None, None);
let chat_conf = ChatConfJson::get_chat_conf();
let mut builder = tauri::Builder::default() let mut builder = tauri::Builder::default()
// https://github.com/tauri-apps/tauri/pull/2736 .plugin(log.build())
.plugin(
LoggerBuilder::new()
.level(log::LevelFilter::Debug)
.with_colors(colors)
.targets([
// LogTarget::LogDir,
// LOG PATH: ~/.chatgpt/ChatGPT.log
LogTarget::Folder(path::home_dir().unwrap().join(".chatgpt")),
LogTarget::Stdout,
LogTarget::Webview,
])
.build(),
)
.plugin(tauri_plugin_positioner::init()) .plugin(tauri_plugin_positioner::init())
.plugin(tauri_plugin_autostart::init( .plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent, MacosLauncher::LaunchAgent,
@@ -61,29 +58,32 @@ async fn main() {
cmd::download, cmd::download,
cmd::save_file, cmd::save_file,
cmd::open_link, cmd::open_link,
cmd::get_chat_conf,
cmd::get_theme,
cmd::reset_chat_conf,
cmd::run_check_update, cmd::run_check_update,
cmd::form_cancel,
cmd::form_confirm,
cmd::form_msg,
cmd::open_file, cmd::open_file,
cmd::get_chat_model_cmd, cmd::get_data,
cmd::parse_prompt, gpt::get_chat_model_cmd,
cmd::sync_prompts, gpt::parse_prompt,
cmd::sync_user_prompts, gpt::sync_prompts,
cmd::window_reload, gpt::sync_user_prompts,
cmd::dalle2_window, gpt::cmd_list,
cmd::cmd_list, gpt::download_list,
cmd::download_list, gpt::get_download_list,
cmd::get_download_list,
fs_extra::metadata, fs_extra::metadata,
conf::cmd::get_app_conf,
conf::cmd::reset_app_conf,
conf::cmd::get_theme,
conf::cmd::form_confirm,
conf::cmd::form_cancel,
conf::cmd::form_msg,
window::cmd::wa_window,
window::cmd::control_window,
window::cmd::window_reload,
window::cmd::dalle2_search_window,
]) ])
.setup(setup::init) .setup(setup::init)
.menu(menu::init()); .menu(menu::init());
if chat_conf.tray { if app_conf.tray {
builder = builder.system_tray(menu::tray_menu()); builder = builder.system_tray(menu::tray_menu());
} }
@@ -91,19 +91,10 @@ async fn main() {
.on_menu_event(menu::menu_handler) .on_menu_event(menu::menu_handler)
.on_system_tray_event(menu::tray_handler) .on_system_tray_event(menu::tray_handler)
.on_window_event(|event| { .on_window_event(|event| {
// https://github.com/tauri-apps/tauri/discussions/2684
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() { if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
let win = event.window(); let win = event.window();
if win.label() == "core" { if win.label() == "core" {
// TODO: https://github.com/tauri-apps/tauri/issues/3084
// event.window().hide().unwrap();
// https://github.com/tauri-apps/tao/pull/517
#[cfg(target_os = "macos")]
event.window().minimize().unwrap(); event.window().minimize().unwrap();
// fix: https://github.com/lencx/ChatGPT/issues/93
#[cfg(not(target_os = "macos"))]
event.window().hide().unwrap();
} else { } else {
win.close().unwrap(); win.close().unwrap();
} }

View File

@@ -75,6 +75,10 @@ function init() {
width: 22px; width: 22px;
height: 22px; height: 22px;
} }
.chatappico.copy {
width: 16px;
height: 16px;
}
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
#download-png-button, #download-pdf-button, #download-html-button { #download-png-button, #download-pdf-button, #download-html-button {
display: none; display: none;

View File

@@ -36,8 +36,22 @@ async function invoke(cmd, args) {
}); });
} }
async function message(message) {
invoke('messageDialog', {
__tauriModule: 'Dialog',
message: {
cmd: 'messageDialog',
message: message.toString(),
title: null,
type: null,
buttonLabel: null
}
});
}
window.uid = uid; window.uid = uid;
window.invoke = invoke; window.invoke = invoke;
window.message = message;
window.transformCallback = transformCallback; window.transformCallback = transformCallback;
async function init() { async function init() {
@@ -45,8 +59,6 @@ async function init() {
document.getElementsByTagName('html')[0].style['font-size'] = '70%'; document.getElementsByTagName('html')[0].style['font-size'] = '70%';
} }
if (__TAURI_METADATA__.__currentWindow.label !== 'core') return;
async function platform() { async function platform() {
return invoke('platform', { return invoke('platform', {
__tauriModule: 'Os', __tauriModule: 'Os',
@@ -54,8 +66,9 @@ async function init() {
}); });
} }
if (__TAURI_METADATA__.__currentWindow.label !== 'tray') {
const _platform = await platform(); const _platform = await platform();
const chatConf = await invoke('get_chat_conf') || {}; const chatConf = await invoke('get_app_conf') || {};
if (/darwin/.test(_platform) && !chatConf.titlebar) { if (/darwin/.test(_platform) && !chatConf.titlebar) {
const topStyleDom = document.createElement("style"); const topStyleDom = document.createElement("style");
topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`; topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`;
@@ -64,10 +77,21 @@ async function init() {
topDom.id = "chatgpt-app-window-top"; topDom.id = "chatgpt-app-window-top";
document.body.appendChild(topDom); document.body.appendChild(topDom);
if (window.location.host === 'chat.openai.com') {
const nav = document.body.querySelector('nav');
if (nav) {
const currentPaddingTop = parseInt(window.getComputedStyle(document.querySelector('nav'), null).getPropertyValue('padding-top').replace('px', ''), 10);
const navStyleDom = document.createElement("style");
navStyleDom.innerHTML = `nav{padding-top:${currentPaddingTop + topDom.clientHeight}px !important}`;
document.head.appendChild(navStyleDom);
}
}
topDom.addEventListener("mousedown", () => invoke("drag_window")); topDom.addEventListener("mousedown", () => invoke("drag_window"));
topDom.addEventListener("touchstart", () => invoke("drag_window")); topDom.addEventListener("touchstart", () => invoke("drag_window"));
topDom.addEventListener("dblclick", () => invoke("fullscreen")); topDom.addEventListener("dblclick", () => invoke("fullscreen"));
} }
}
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const origin = e.target.closest("a"); const origin = e.target.closest("a");
@@ -77,22 +101,78 @@ async function init() {
} }
}); });
document.addEventListener('wheel', function(event) { if (window.location.host === 'chat.openai.com') {
const deltaX = event.wheelDeltaX;
if (Math.abs(deltaX) >= 50) {
if (deltaX > 0) {
window.history.go(-1);
} else {
window.history.go(1);
}
}
});
window.__sync_prompts = async function() { window.__sync_prompts = async function() {
await invoke('sync_prompts', { time: Date.now() }); await invoke('sync_prompts', { time: Date.now() });
} }
} }
coreZoom();
}
function coreZoom() {
const styleDom = document.createElement('style');
styleDom.innerHTML = `
#ZoomTopTip {
display: none;
position: fixed;
top: 0;
right: 20px;
background: #2a2a2a;
color: #fafafa;
padding: 20px 15px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
font-size: 16px;
font-weight: bold;
z-index: 999999;
box-shadow: 0 2px 2px 2px #d8d8d8;
}
.ZoomTopTipAni {
transition: opacity 200ms, display 200ms;
display: none;
opacity: 0;
}
`;
document.head.append(styleDom);
const zoomTipDom = document.createElement('div');
zoomTipDom.id = 'ZoomTopTip';
document.body.appendChild(zoomTipDom);
function zoom(callback) {
if (window.zoomSetTimeout) clearTimeout(window.zoomSetTimeout);
const htmlZoom = window.localStorage.getItem("htmlZoom") || "100%";
const html = document.getElementsByTagName("html")[0];
const zoom = callback(htmlZoom);
html.style.zoom = zoom;
window.localStorage.setItem("htmlZoom", zoom);
zoomTipDom.innerHTML = zoom;
zoomTipDom.style.display = 'block';
zoomTipDom.classList.remove('ZoomTopTipAni');
window.zoomSetTimeout = setTimeout(() => {
zoomTipDom.classList.add('ZoomTopTipAni');
}, 2500);
}
function zoomDefault() {
const htmlZoom = window.localStorage.getItem("htmlZoom");
if (htmlZoom) {
document.getElementsByTagName("html")[0].style.zoom = htmlZoom;
}
}
function zoomIn() {
zoom((htmlZoom) => `${Math.min(parseInt(htmlZoom) + 10, 200)}%`);
}
function zoomOut() {
zoom((htmlZoom) => `${Math.max(parseInt(htmlZoom) - 10, 30)}%`);
}
function zoom0() {
zoom(() => `100%`);
}
zoomDefault();
window.__zoomIn = zoomIn;
window.__zoomOut = zoomOut;
window.__zoom0 = zoom0;
}
if ( if (
document.readyState === "complete" || document.readyState === "complete" ||
document.readyState === "interactive" document.readyState === "interactive"

View File

@@ -2,11 +2,13 @@
async function init() { async function init() {
const buttonOuterHTMLFallback = `<button class="btn flex justify-center gap-2 btn-neutral" id="download-png-button">Try Again</button>`; const buttonOuterHTMLFallback = `<button class="btn flex justify-center gap-2 btn-neutral" id="download-png-button">Try Again</button>`;
if (window.innerWidth < 767) return; removeButtons();
const chatConf = await invoke('get_chat_conf') || {};
if (window.buttonsInterval) { if (window.buttonsInterval) {
clearInterval(window.buttonsInterval); clearInterval(window.buttonsInterval);
} }
if (window.innerWidth < 767) return;
const chatConf = await invoke('get_app_conf') || {};
window.buttonsInterval = setInterval(() => { window.buttonsInterval = setInterval(() => {
const actionsArea = document.querySelector("form>div>div"); const actionsArea = document.querySelector("form>div>div");
if (!actionsArea) { if (!actionsArea) {
@@ -20,12 +22,15 @@ async function init() {
TryAgainButton = parentNode.querySelector("button"); TryAgainButton = parentNode.querySelector("button");
} }
addActionsButtons(actionsArea, TryAgainButton, chatConf); addActionsButtons(actionsArea, TryAgainButton, chatConf);
copyBtns();
} else if (shouldRemoveButtons()) { } else if (shouldRemoveButtons()) {
removeButtons(); removeButtons();
} }
}, 1000); }, 1000);
} }
window.addEventListener('resize', init);
const Format = { const Format = {
PNG: "png", PNG: "png",
PDF: "pdf", PDF: "pdf",
@@ -200,7 +205,7 @@ class Elements {
} }
init() { init() {
// this.threadWrapper = document.querySelector(".cdfdFe"); // this.threadWrapper = document.querySelector(".cdfdFe");
this.spacer = document.querySelector(".w-full.h-48.flex-shrink-0"); this.spacer = document.querySelector("[class*='h-48'].w-full.flex-shrink-0");
this.thread = document.querySelector( this.thread = document.querySelector(
"[class*='react-scroll-to-bottom']>[class*='react-scroll-to-bottom']>div" "[class*='react-scroll-to-bottom']>[class*='react-scroll-to-bottom']>div"
); );
@@ -268,10 +273,44 @@ function setIcon(type) {
// link: `<svg class="chatappico" viewBox="0 0 1024 1024"><path d="M1007.382 379.672L655.374 75.702C624.562 49.092 576 70.694 576 112.03v160.106C254.742 275.814 0 340.2 0 644.652c0 122.882 79.162 244.618 166.666 308.264 27.306 19.862 66.222-5.066 56.154-37.262C132.132 625.628 265.834 548.632 576 544.17V720c0 41.4 48.6 62.906 79.374 36.328l352.008-304c22.142-19.124 22.172-53.506 0-72.656z" p-id="8506" fill="currentColor"></path></svg>`, // link: `<svg class="chatappico" viewBox="0 0 1024 1024"><path d="M1007.382 379.672L655.374 75.702C624.562 49.092 576 70.694 576 112.03v160.106C254.742 275.814 0 340.2 0 644.652c0 122.882 79.162 244.618 166.666 308.264 27.306 19.862 66.222-5.066 56.154-37.262C132.132 625.628 265.834 548.632 576 544.17V720c0 41.4 48.6 62.906 79.374 36.328l352.008-304c22.142-19.124 22.172-53.506 0-72.656z" p-id="8506" fill="currentColor"></path></svg>`,
png: `<svg class="chatappico" viewBox="0 0 1070 1024"><path d="M981.783273 0H85.224727C38.353455 0 0 35.374545 0 83.083636v844.893091c0 47.616 38.353455 86.574545 85.178182 86.574546h903.633454c46.917818 0 81.733818-38.958545 81.733819-86.574546V83.083636C1070.592 35.374545 1028.701091 0 981.783273 0zM335.825455 135.912727c74.193455 0 134.330182 60.974545 134.330181 136.285091 0 75.170909-60.136727 136.192-134.330181 136.192-74.286545 0-134.516364-61.021091-134.516364-136.192 0-75.264 60.229818-136.285091 134.516364-136.285091z m-161.512728 745.937455a41.890909 41.890909 0 0 1-27.648-10.379637 43.752727 43.752727 0 0 1-4.654545-61.067636l198.097454-255.162182a42.123636 42.123636 0 0 1 57.716364-6.702545l116.549818 128.139636 286.906182-352.814545c14.615273-18.711273 90.251636-106.775273 135.866182-6.935273 0.093091-0.093091 0.093091 112.965818 0.232727 247.761455 0.093091 140.8 0.093091 317.067636 0.093091 317.067636-1.024-0.093091-762.740364 0.093091-763.112727 0.093091z" fill="currentColor"></path></svg>`, png: `<svg class="chatappico" viewBox="0 0 1070 1024"><path d="M981.783273 0H85.224727C38.353455 0 0 35.374545 0 83.083636v844.893091c0 47.616 38.353455 86.574545 85.178182 86.574546h903.633454c46.917818 0 81.733818-38.958545 81.733819-86.574546V83.083636C1070.592 35.374545 1028.701091 0 981.783273 0zM335.825455 135.912727c74.193455 0 134.330182 60.974545 134.330181 136.285091 0 75.170909-60.136727 136.192-134.330181 136.192-74.286545 0-134.516364-61.021091-134.516364-136.192 0-75.264 60.229818-136.285091 134.516364-136.285091z m-161.512728 745.937455a41.890909 41.890909 0 0 1-27.648-10.379637 43.752727 43.752727 0 0 1-4.654545-61.067636l198.097454-255.162182a42.123636 42.123636 0 0 1 57.716364-6.702545l116.549818 128.139636 286.906182-352.814545c14.615273-18.711273 90.251636-106.775273 135.866182-6.935273 0.093091-0.093091 0.093091 112.965818 0.232727 247.761455 0.093091 140.8 0.093091 317.067636 0.093091 317.067636-1.024-0.093091-762.740364 0.093091-763.112727 0.093091z" fill="currentColor"></path></svg>`,
pdf: `<svg class="chatappico pdf" viewBox="0 0 1024 1024"><path d="M821.457602 118.382249H205.725895c-48.378584 0-87.959995 39.583368-87.959996 87.963909v615.731707c0 48.378584 39.581411 87.959995 87.959996 87.959996h615.733664c48.380541 0 87.961952-39.581411 87.961952-87.959996V206.346158c-0.001957-48.378584-39.583368-87.963909-87.963909-87.963909zM493.962468 457.544987c-10.112054 32.545237-21.72487 82.872662-38.806571 124.248336-8.806957 22.378397-8.380404 18.480717-15.001764 32.609808l5.71738-1.851007c58.760658-16.443827 99.901532-20.519564 138.162194-27.561607-7.67796-6.06371-14.350194-10.751884-19.631237-15.586807-26.287817-29.101504-35.464584-34.570387-70.440002-111.862636v0.003913z m288.36767 186.413594c-7.476424 8.356924-20.670227 13.191847-40.019704 13.191847-33.427694 0-63.808858-9.229597-107.79277-31.660824-75.648648 8.356924-156.097 17.214754-201.399704 31.729308-2.199293 0.876587-4.832967 1.759043-7.916674 3.077836-54.536215 93.237125-95.031389 132.767663-130.621199 131.19646-11.286054-0.49895-27.694661-7.044-32.973748-10.11988l-6.52157-6.196764-2.29517-4.353583c-3.07588-7.91863-3.954423-15.395054-2.197337-23.751977 4.838837-23.309771 29.907651-60.251638 82.686779-93.237126 8.356924-6.159587 27.430511-15.897917 45.020944-24.25484 13.311204-21.177004 19.45905-34.744531 36.341171-72.259702 19.102937-45.324228 36.505531-99.492589 47.500041-138.191543v-0.44025c-16.267727-53.219378-25.945401-89.310095-9.67376-147.80856 3.958337-16.71189 18.46702-33.864031 34.748444-33.864031h10.552304c10.115967 0 19.791684 3.520043 26.829814 10.552304 29.029107 29.031064 15.39114 103.824649 0.8805 162.323113-0.8805 2.63563-1.322707 4.832967-1.761 6.153717 17.59239 49.697378 45.400538 98.774492 73.108895 121.647926 11.436717 8.791304 22.638634 18.899444 36.71098 26.814161 19.791684-2.20125 37.517128-4.11487 55.547812-4.11487 54.540128 0 87.525615 9.67963 100.279169 30.351814 4.400543 7.034217 6.595923 15.389184 5.281043 24.1844-0.44025 10.996467-4.39663 21.112434-12.31526 29.031064z m-27.796407-36.748157c-4.394673-4.398587-17.024957-16.936907-78.601259-16.936907-3.073923 0-10.622744-0.784623-14.57521 3.612007 32.104987 14.072347 62.830525 24.757704 83.058545 24.757703 3.083707 0 5.72325-0.442207 8.356923-0.876586h1.759044c2.20125-0.8805 3.520043-1.324663 3.960293-5.71738-0.87463-1.324663-1.757087-3.083707-3.958336-4.838837z m-387.124553 63.041845c-9.237424 5.27713-16.71189 10.112054-21.112433 13.634053-31.226444 28.586901-51.018128 57.616008-53.217422 74.331812 19.789727-6.59788 45.737084-35.626987 74.329855-87.961952v-0.003913z m125.574957-297.822284l2.197336-1.761c3.079793-14.072347 5.232127-29.189554 7.87167-38.869184l1.318794-7.036174c4.39663-25.070771 2.71781-39.720334-4.76057-50.272637l-6.59788-2.20125a57.381208 57.381208 0 0 0-3.079794 5.27713c-7.474467 18.47289-7.063567 55.283661 3.0524 94.865072l-0.001956-0.001957z" fill="currentColor"></path></svg>`, pdf: `<svg class="chatappico pdf" viewBox="0 0 1024 1024"><path d="M821.457602 118.382249H205.725895c-48.378584 0-87.959995 39.583368-87.959996 87.963909v615.731707c0 48.378584 39.581411 87.959995 87.959996 87.959996h615.733664c48.380541 0 87.961952-39.581411 87.961952-87.959996V206.346158c-0.001957-48.378584-39.583368-87.963909-87.963909-87.963909zM493.962468 457.544987c-10.112054 32.545237-21.72487 82.872662-38.806571 124.248336-8.806957 22.378397-8.380404 18.480717-15.001764 32.609808l5.71738-1.851007c58.760658-16.443827 99.901532-20.519564 138.162194-27.561607-7.67796-6.06371-14.350194-10.751884-19.631237-15.586807-26.287817-29.101504-35.464584-34.570387-70.440002-111.862636v0.003913z m288.36767 186.413594c-7.476424 8.356924-20.670227 13.191847-40.019704 13.191847-33.427694 0-63.808858-9.229597-107.79277-31.660824-75.648648 8.356924-156.097 17.214754-201.399704 31.729308-2.199293 0.876587-4.832967 1.759043-7.916674 3.077836-54.536215 93.237125-95.031389 132.767663-130.621199 131.19646-11.286054-0.49895-27.694661-7.044-32.973748-10.11988l-6.52157-6.196764-2.29517-4.353583c-3.07588-7.91863-3.954423-15.395054-2.197337-23.751977 4.838837-23.309771 29.907651-60.251638 82.686779-93.237126 8.356924-6.159587 27.430511-15.897917 45.020944-24.25484 13.311204-21.177004 19.45905-34.744531 36.341171-72.259702 19.102937-45.324228 36.505531-99.492589 47.500041-138.191543v-0.44025c-16.267727-53.219378-25.945401-89.310095-9.67376-147.80856 3.958337-16.71189 18.46702-33.864031 34.748444-33.864031h10.552304c10.115967 0 19.791684 3.520043 26.829814 10.552304 29.029107 29.031064 15.39114 103.824649 0.8805 162.323113-0.8805 2.63563-1.322707 4.832967-1.761 6.153717 17.59239 49.697378 45.400538 98.774492 73.108895 121.647926 11.436717 8.791304 22.638634 18.899444 36.71098 26.814161 19.791684-2.20125 37.517128-4.11487 55.547812-4.11487 54.540128 0 87.525615 9.67963 100.279169 30.351814 4.400543 7.034217 6.595923 15.389184 5.281043 24.1844-0.44025 10.996467-4.39663 21.112434-12.31526 29.031064z m-27.796407-36.748157c-4.394673-4.398587-17.024957-16.936907-78.601259-16.936907-3.073923 0-10.622744-0.784623-14.57521 3.612007 32.104987 14.072347 62.830525 24.757704 83.058545 24.757703 3.083707 0 5.72325-0.442207 8.356923-0.876586h1.759044c2.20125-0.8805 3.520043-1.324663 3.960293-5.71738-0.87463-1.324663-1.757087-3.083707-3.958336-4.838837z m-387.124553 63.041845c-9.237424 5.27713-16.71189 10.112054-21.112433 13.634053-31.226444 28.586901-51.018128 57.616008-53.217422 74.331812 19.789727-6.59788 45.737084-35.626987 74.329855-87.961952v-0.003913z m125.574957-297.822284l2.197336-1.761c3.079793-14.072347 5.232127-29.189554 7.87167-38.869184l1.318794-7.036174c4.39663-25.070771 2.71781-39.720334-4.76057-50.272637l-6.59788-2.20125a57.381208 57.381208 0 0 0-3.079794 5.27713c-7.474467 18.47289-7.063567 55.283661 3.0524 94.865072l-0.001956-0.001957z" fill="currentColor"></path></svg>`,
md: `<svg class="chatappico md" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1380" width="200" height="200"><path d="M128 128h768a42.666667 42.666667 0 0 1 42.666667 42.666667v682.666666a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V170.666667a42.666667 42.666667 0 0 1 42.666667-42.666667z m170.666667 533.333333v-170.666666l85.333333 85.333333 85.333333-85.333333v170.666666h85.333334v-298.666666h-85.333334l-85.333333 85.333333-85.333333-85.333333H213.333333v298.666666h85.333334z m469.333333-128v-170.666666h-85.333333v170.666666h-85.333334l128 128 128-128h-85.333333z" p-id="1381" fill="currentColor"></path></svg>` md: `<svg class="chatappico md" viewBox="0 0 1024 1024" width="200" height="200"><path d="M128 128h768a42.666667 42.666667 0 0 1 42.666667 42.666667v682.666666a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V170.666667a42.666667 42.666667 0 0 1 42.666667-42.666667z m170.666667 533.333333v-170.666666l85.333333 85.333333 85.333333-85.333333v170.666666h85.333334v-298.666666h-85.333334l-85.333333 85.333333-85.333333-85.333333H213.333333v298.666666h85.333334z m469.333333-128v-170.666666h-85.333333v170.666666h-85.333334l128 128 128-128h-85.333333z" p-id="1381" fill="currentColor"></path></svg>`,
copy: `<svg class="chatappico copy" stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>`
}[type]; }[type];
} }
function copyBtns() {
Array.from(document.querySelectorAll("main >div>div>div>div>div"))
.forEach(i => {
if (i.querySelector('.chat-item-copy')) return;
if (!i.querySelector('button.rounded-md')) return;
const btn = i.querySelector('button.rounded-md').cloneNode(true);
btn.classList.add('chat-item-copy');
btn.title = 'Copy to clipboard';
btn.innerHTML = setIcon('copy');
i.querySelector('.self-end').appendChild(btn);
btn.onclick = () => {
copyToClipboard(i?.innerText?.trim() || '');
}
})
}
function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
} else {
var textarea = document.createElement('textarea');
document.body.appendChild(textarea);
textarea.style.position = 'fixed';
textarea.style.clip = 'rect(0 0 0 0)';
textarea.style.top = '10px';
textarea.value = text;
textarea.select();
document.execCommand('copy', true);
document.body.removeChild(textarea);
}
message('Copied to clipboard');
}
if ( if (
document.readyState === "complete" || document.readyState === "complete" ||
document.readyState === "interactive" document.readyState === "interactive"

View File

@@ -1,7 +1,7 @@
// *** Core Script - DALL·E 2 Core *** // *** Core Script - DALL·E 2 Core ***
async function init() { async function init() {
const chatConf = await invoke('get_chat_conf') || {}; const chatConf = await invoke('get_app_conf') || {};
if (!chatConf.popup_search) return; if (!chatConf.popup_search) return;
if (!window.FloatingUIDOM) return; if (!window.FloatingUIDOM) return;
@@ -33,7 +33,7 @@ async function init() {
document.body.addEventListener('mousedown', async (e) => { document.body.addEventListener('mousedown', async (e) => {
selectionMenu.style.display = 'none'; selectionMenu.style.display = 'none';
if (e.target.id === 'chagpt-selection-menu') { if (e.target.id === 'chagpt-selection-menu') {
await invoke('dalle2_window', { query: encodeURIComponent(window.__DALLE2_CONTENT__) }); await invoke('dalle2_search_window', { query: encodeURIComponent(window.__DALLE2_CONTENT__) });
} else { } else {
delete window.__DALLE2_CONTENT__; delete window.__DALLE2_CONTENT__;
} }

View File

@@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use log::info; use log::{error, info};
use regex::Regex; use regex::Regex;
use serde_json::Value; use serde_json::Value;
use std::{ use std::{
@@ -11,14 +11,13 @@ use std::{
use tauri::updater::UpdateResponse; use tauri::updater::UpdateResponse;
use tauri::{utils::config::Config, AppHandle, Manager, Wry}; use tauri::{utils::config::Config, AppHandle, Manager, Wry};
pub fn chat_root() -> PathBuf { pub fn app_root() -> PathBuf {
tauri::api::path::home_dir().unwrap().join(".chatgpt") tauri::api::path::home_dir().unwrap().join(".chatgpt")
} }
pub fn get_tauri_conf() -> Option<Config> { pub fn get_tauri_conf() -> Option<Config> {
let config_file = include_str!("../tauri.conf.json"); let config_file = include_str!("../tauri.conf.json");
let config: Config = let config: Config = serde_json::from_str(config_file).expect("failed to parse tauri.conf.json");
serde_json::from_str(config_file).expect("failed to parse tauri.conf.json");
Some(config) Some(config)
} }
@@ -34,7 +33,7 @@ pub fn create_file(path: &Path) -> Result<File> {
} }
pub fn create_chatgpt_prompts() { pub fn create_chatgpt_prompts() {
let sync_file = chat_root().join("cache_model").join("chatgpt_prompts.json"); let sync_file = app_root().join("cache_model").join("chatgpt_prompts.json");
if !exists(&sync_file) { if !exists(&sync_file) {
create_file(&sync_file).unwrap(); create_file(&sync_file).unwrap();
fs::write(&sync_file, "[]").unwrap(); fs::write(&sync_file, "[]").unwrap();
@@ -42,10 +41,17 @@ pub fn create_chatgpt_prompts() {
} }
pub fn script_path() -> PathBuf { pub fn script_path() -> PathBuf {
let script_file = chat_root().join("main.js"); let script_file = app_root().join("main.js");
if !exists(&script_file) { if !exists(&script_file) {
create_file(&script_file).unwrap(); create_file(&script_file).unwrap();
fs::write(&script_file, format!("// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');", &script_file.to_string_lossy())).unwrap(); fs::write(
&script_file,
format!(
"// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');",
&script_file.to_string_lossy()
),
)
.unwrap();
} }
script_file script_file
@@ -60,26 +66,43 @@ pub fn user_script() -> String {
} }
pub fn open_file(path: PathBuf) { pub fn open_file(path: PathBuf) {
info!("open_file: {}", path.to_string_lossy()); let pathname = convert_path(path.to_str().unwrap());
info!("open_file: {}", pathname);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
Command::new("open").arg("-R").arg(path).spawn().unwrap(); Command::new("open")
.arg("-R")
.arg(pathname)
.spawn()
.unwrap();
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
Command::new("explorer") Command::new("explorer.exe")
.arg("/select,") .arg("/select,")
.arg(path) .arg(pathname)
.spawn() .spawn()
.unwrap(); .unwrap();
// https://askubuntu.com/a/31071 // https://askubuntu.com/a/31071
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
Command::new("xdg-open").arg(path).spawn().unwrap(); Command::new("xdg-open").arg(pathname).spawn().unwrap();
}
pub fn convert_path(path_str: &str) -> String {
if cfg!(target_os = "windows") {
path_str.replace('/', "\\")
} else {
String::from(path_str)
}
} }
pub fn clear_conf(app: &tauri::AppHandle) { pub fn clear_conf(app: &tauri::AppHandle) {
let root = chat_root(); let root = app_root();
let app2 = app.clone(); let msg = format!(
let msg = format!("Path: {}\nAre you sure to clear all ChatGPT configurations? Please backup in advance if necessary!", root.to_string_lossy()); "Path: {}\n
Are you sure you want to clear all ChatGPT configurations? Performing this operation data can not be restored, please back up in advance.\n
Note: The application will exit automatically after the configuration cleanup!",
root.to_string_lossy()
);
tauri::api::dialog::ask( tauri::api::dialog::ask(
app.get_window("core").as_ref(), app.get_window("core").as_ref(),
"Clear Config", "Clear Config",
@@ -87,7 +110,7 @@ pub fn clear_conf(app: &tauri::AppHandle) {
move |is_ok| { move |is_ok| {
if is_ok { if is_ok {
fs::remove_dir_all(root).unwrap(); fs::remove_dir_all(root).unwrap();
tauri::api::process::restart(&app2.env()); std::process::exit(0);
} }
}, },
); );
@@ -122,7 +145,7 @@ pub async fn get_data(
if is_ok { if is_ok {
Ok(Some(body)) Ok(Some(body))
} else { } else {
info!("chatgpt_http_error: {}", body); error!("chatgpt_http: {}", body);
if let Some(v) = app { if let Some(v) = app {
tauri::api::dialog::message(v.get_window("core").as_ref(), "ChatGPT HTTP", body); tauri::api::dialog::message(v.get_window("core").as_ref(), "ChatGPT HTTP", body);
} }
@@ -133,8 +156,7 @@ pub async fn get_data(
pub fn run_check_update(app: AppHandle<Wry>, silent: bool, has_msg: Option<bool>) { pub fn run_check_update(app: AppHandle<Wry>, silent: bool, has_msg: Option<bool>) {
info!("run_check_update: silent={} has_msg={:?}", silent, has_msg); info!("run_check_update: silent={} has_msg={:?}", silent, has_msg);
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let result = app.updater().check().await; if let Ok(update_resp) = app.updater().check().await {
let update_resp = result.unwrap();
if update_resp.is_update_available() { if update_resp.is_update_available() {
if silent { if silent {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
@@ -154,6 +176,7 @@ pub fn run_check_update(app: AppHandle<Wry>, silent: bool, has_msg: Option<bool>
); );
} }
} }
}
}); });
} }

View File

@@ -7,7 +7,7 @@
}, },
"package": { "package": {
"productName": "ChatGPT", "productName": "ChatGPT",
"version": "0.9.2" "version": "0.10.2"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

41
src/components/FilePath/index.tsx vendored Normal file
View File

@@ -0,0 +1,41 @@
import { FC, useEffect, useState } from 'react';
import clsx from 'clsx';
import { path, shell } from '@tauri-apps/api';
import { chatRoot } from '@/utils';
interface FilePathProps {
paths?: string;
label?: string;
className?: string;
content?: string;
url?: string;
}
const FilePath: FC<FilePathProps> = ({ className, label = 'PATH', paths = '', url, content }) => {
const [filePath, setPath] = useState('');
useEffect(() => {
if (!path && !url) return;
(async () => {
if (url) {
setPath(url);
return;
}
setPath(await path.join(await chatRoot(), ...paths.split('/').filter((i) => !!i)));
})();
}, [url, paths]);
return (
<div className={clsx(className, 'chat-file-path')}>
<div>
{label}:{' '}
<a onClick={() => shell.open(filePath)} title={filePath}>
{content ? content : filePath}
</a>
</div>
</div>
);
};
export default FilePath;

48
src/components/Markdown/Editor.tsx vendored Normal file
View File

@@ -0,0 +1,48 @@
import { FC, useEffect, useState } from 'react';
import Editor from '@monaco-editor/react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import Markdown from '@/components/Markdown';
import './index.scss';
interface MarkdownEditorProps {
value?: string;
onChange?: (v: string) => void;
mode?: string;
}
const MarkdownEditor: FC<MarkdownEditorProps> = ({ value = '', onChange, mode = 'split' }) => {
const [content, setContent] = useState(value);
useEffect(() => {
setContent(value);
onChange && onChange(value);
}, [value]);
const handleEdit = (e: any) => {
setContent(e);
onChange && onChange(e);
};
const isSplit = mode === 'split';
return (
<div className="md-main">
<PanelGroup direction="horizontal">
{['md', 'split'].includes(mode) && (
<Panel>
<Editor language="markdown" value={content} onChange={handleEdit} />
</Panel>
)}
{isSplit && <PanelResizeHandle className="resize-handle" />}
{['doc', 'split'].includes(mode) && (
<Panel>
<Markdown className="edit-preview">{content}</Markdown>
</Panel>
)}
</PanelGroup>
</div>
);
};
export default MarkdownEditor;

33
src/components/Markdown/index.scss vendored Normal file
View File

@@ -0,0 +1,33 @@
.markdown-body {
height: 100%;
overflow: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial,
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
&.edit-preview {
padding: 10px;
font-size: 14px;
}
pre,
code {
font-family: monospace, monospace;
}
}
.md-main {
height: calc(100vh - 130px);
overflow: hidden;
}
.resize-handle {
width: 0.25rem;
transition: 250ms linear background-color;
background-color: #7f8082;
outline: none;
&:hover,
&[data-resize-handle-active] {
background-color: #5194eb;
}
}

52
src/components/Markdown/index.tsx vendored Normal file
View File

@@ -0,0 +1,52 @@
import { FC } from 'react';
import clsx from 'clsx';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import SyntaxHighlighter from 'react-syntax-highlighter';
import agate from 'react-syntax-highlighter/dist/esm/styles/hljs/agate';
import 'github-markdown-css';
import './index.scss';
interface MarkdownProps {
children: string;
className?: string;
}
const Markdown: FC<MarkdownProps> = ({ children, className }) => {
return (
<div className={clsx(className, 'markdown-body')}>
<div>
<ReactMarkdown
children={children}
linkTarget="_blank"
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, '')}
style={agate as any}
language={match[1]}
showLineNumbers
lineNumberStyle={{ color: '#999' }}
PreTag="div"
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
}}
/>
</div>
</div>
);
};
export default Markdown;

98
src/components/SwitchOrigin/index.tsx vendored Normal file
View File

@@ -0,0 +1,98 @@
import { FC } from 'react';
import { Link } from 'react-router-dom';
import { Form, Select, Tag, Tooltip, Switch } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import useJson from '@/hooks/useJson';
import { DISABLE_AUTO_COMPLETE, CHAT_AWESOME_JSON } from '@/utils';
interface SwitchOriginProps {
name: string;
}
const SwitchOrigin: FC<SwitchOriginProps> = ({ name }) => {
const { json: list = [] } = useJson<any[]>(CHAT_AWESOME_JSON);
const form = Form.useFormInstance();
const labelName = `(${name === 'main' ? 'Main' : 'SystemTray'})`;
const dashboardName = `${name}_dashboard`;
const originName = `${name}_origin`;
const isEnable = Form.useWatch(dashboardName, form);
let urlList = [{ title: 'ChatGPT', url: 'https://chat.openai.com', init: true }];
if (Array.isArray(list)) {
urlList = urlList.concat(list);
}
return (
<>
<Form.Item
label={
<span>
Dashboard {labelName}{' '}
<Tooltip
title={
<div>
<p>
<b>Set Dashboard as the application default window.</b>
</p>
<p>
If this is enabled, the <Tag color="blue">Switch Origin {labelName}</Tag>{' '}
setting will be invalid.
</p>
<p>
If you want to add a new URL to the dashboard, add it in the{' '}
<Link to="/awesome">Awesome</Link> menu and make sure it is enabled.
</p>
</div>
}
>
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
</Tooltip>
</span>
}
name={dashboardName}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label={
<span>
Switch Origin {labelName}{' '}
<Tooltip
title={
<div>
<p>
<b>Set a single URL as the application default window.</b>
</p>
<p>
If you need to set a new URL as the application loading window, please add the
URL in the <Link to="/awesome">Awesome</Link> menu and then select it.
</p>
</div>
}
>
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
</Tooltip>
</span>
}
name={originName}
>
<Select disabled={isEnable} showSearch {...DISABLE_AUTO_COMPLETE} optionLabelProp="url">
{urlList.map((i, idx) => (
<Select.Option
key={`${idx}_${i.url}`}
label={i.title}
value={i.url}
title={`${i.title}${i.init ? '(Built-in)' : ''}: ${i.url}`}
>
<Tag color={i.init ? 'orange' : 'geekblue'}>{i.title}</Tag> {i.url}
</Select.Option>
))}
</Select>
</Form.Item>
</>
);
};
export default SwitchOrigin;

View File

@@ -8,9 +8,11 @@ import { DISABLE_AUTO_COMPLETE } from '@/utils';
interface TagsProps { interface TagsProps {
value?: string[]; value?: string[];
onChange?: (v: string[]) => void; onChange?: (v: string[]) => void;
addTxt?: string;
max?: number;
} }
const Tags: FC<TagsProps> = ({ value = [], onChange }) => { const Tags: FC<TagsProps> = ({ max = 99, value = [], onChange, addTxt = 'New Tag' }) => {
const [tags, setTags] = useState<string[]>(value); const [tags, setTags] = useState<string[]>(value);
const [inputVisible, setInputVisible] = useState<boolean>(false); const [inputVisible, setInputVisible] = useState<boolean>(false);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
@@ -18,7 +20,7 @@ const Tags: FC<TagsProps> = ({ value = [], onChange }) => {
useEffect(() => { useEffect(() => {
setTags(value); setTags(value);
}, [value]) }, [value]);
useEffect(() => { useEffect(() => {
if (inputVisible) { if (inputVisible) {
@@ -86,9 +88,9 @@ const Tags: FC<TagsProps> = ({ value = [], onChange }) => {
{...DISABLE_AUTO_COMPLETE} {...DISABLE_AUTO_COMPLETE}
/> />
)} )}
{!inputVisible && ( {!inputVisible && tags.length < max && (
<Tag onClick={showInput} className="chat-tag-new"> <Tag onClick={showInput} className="chat-tag-new">
<PlusOutlined /> New Tag <PlusOutlined /> {addTxt}
</Tag> </Tag>
)} )}
</> </>

View File

@@ -20,7 +20,7 @@ export default function useChatModel(key: string, file = CHAT_MODEL_JSON) {
oData[key] = data; oData[key] = data;
await writeJSON(file, oData); await writeJSON(file, oData);
setModelJson(oData); setModelJson(oData);
} };
return { modelJson, modelSet, modelData: modelJson?.[key] || [] }; return { modelJson, modelSet, modelData: modelJson?.[key] || [] };
} }
@@ -40,12 +40,16 @@ export function useCacheModel(file = '') {
await writeJSON(newFile ? newFile : file, data, { isRoot: true }); await writeJSON(newFile ? newFile : file, data, { isRoot: true });
setModelCacheJson(data); setModelCacheJson(data);
await modelCacheCmd(); await modelCacheCmd();
} };
const modelCacheCmd = async () => { const modelCacheCmd = async () => {
// Generate the `chat.model.cmd.json` file and refresh the page for the slash command to take effect. // Generate the `chat.model.cmd.json` file and refresh the page for the slash command to take effect.
const list = await invoke('cmd_list'); const list = await invoke('cmd_list');
await writeJSON(CHAT_MODEL_CMD_JSON, { name: 'ChatGPT CMD', last_updated: Date.now(), data: list }); await writeJSON(CHAT_MODEL_CMD_JSON, {
name: 'ChatGPT CMD',
last_updated: Date.now(),
data: list,
});
await invoke('window_reload', { label: 'core' }); await invoke('window_reload', { label: 'core' });
await invoke('window_reload', { label: 'tray' }); await invoke('window_reload', { label: 'tray' });
}; };

View File

@@ -58,17 +58,16 @@ export const EditRow: FC<EditRowProps> = ({ rowKey, row, actions }) => {
setEdit(true); setEdit(true);
}; };
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setVal(e.target.value) setVal(e.target.value);
}; };
const handleSave = () => { const handleSave = () => {
setEdit(false); setEdit(false);
row[rowKey] = val?.trim(); row[rowKey] = val?.trim();
actions?.setRecord(row, 'rowedit') actions?.setRecord(row, 'rowedit');
}; };
return isEdit return isEdit ? (
? (
<Input <Input
value={val} value={val}
autoFocus autoFocus
@@ -76,8 +75,9 @@ export const EditRow: FC<EditRowProps> = ({ rowKey, row, actions }) => {
{...DISABLE_AUTO_COMPLETE} {...DISABLE_AUTO_COMPLETE}
onPressEnter={handleSave} onPressEnter={handleSave}
/> />
) ) : (
: ( <div className="rowedit" onClick={handleEdit}>
<div className='rowedit' onClick={handleEdit}>{val}</div> {val}
</div>
); );
}; };

25
src/hooks/useData.ts vendored
View File

@@ -8,7 +8,7 @@ export default function useData(oData: any[]) {
useEffect(() => { useEffect(() => {
opInit(oData); opInit(oData);
}, []) }, []);
const opAdd = (val: any) => { const opAdd = (val: any) => {
const v = [val, ...opData]; const v = [val, ...opData];
@@ -18,19 +18,25 @@ export default function useData(oData: any[]) {
const opInit = (val: any[] = []) => { const opInit = (val: any[] = []) => {
if (!val || !Array.isArray(val)) return; if (!val || !Array.isArray(val)) return;
const nData = val.map(i => ({ [safeKey]: v4(), ...i })); const nData = val.map((i) => ({ [safeKey]: v4(), ...i }));
setData(nData); setData(nData);
}; };
const opRemove = (id: string) => { const opRemove = (id: string) => {
const nData = opData.filter(i => i[safeKey] !== id); const nData = opData.filter((i) => i[safeKey] !== id);
setData(nData);
return nData;
};
const opRemoveItems = (ids: string[]) => {
const nData = opData.filter((i) => !ids.includes(i[safeKey]));
setData(nData); setData(nData);
return nData; return nData;
}; };
const opReplace = (id: string, data: any) => { const opReplace = (id: string, data: any) => {
const nData = [...opData]; const nData = [...opData];
const idx = opData.findIndex(v => v[safeKey] === id); const idx = opData.findIndex((v) => v[safeKey] === id);
nData[idx] = data; nData[idx] = data;
setData(nData); setData(nData);
return nData; return nData;
@@ -51,5 +57,14 @@ export default function useData(oData: any[]) {
return nData; return nData;
}; };
return { opSafeKey: safeKey, opInit, opReplace, opAdd, opRemove, opData, opReplaceItems }; return {
opSafeKey: safeKey,
opInit,
opReplace,
opAdd,
opRemove,
opRemoveItems,
opData,
opReplaceItems,
};
} }

View File

@@ -8,5 +8,5 @@ export default function useInit(callback: () => void) {
callback(); callback();
isInit.current = false; isInit.current = false;
} }
}) });
} }

View File

@@ -7,14 +7,17 @@ import { safeKey } from '@/hooks/useData';
type rowSelectionOptions = { type rowSelectionOptions = {
key: 'id' | string; key: 'id' | string;
rowType: 'id' | 'row' | 'all'; rowType: 'id' | 'row' | 'all';
} };
export function useTableRowSelection(options: Partial<rowSelectionOptions> = {}) { export function useTableRowSelection(options: Partial<rowSelectionOptions> = {}) {
const { key = 'id', rowType = 'id' } = options; const { key = 'id', rowType = 'id' } = options;
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [selectedRowIDs, setSelectedRowIDs] = useState<string[]>([]); const [selectedRowIDs, setSelectedRowIDs] = useState<string[]>([]);
const [selectedRows, setSelectedRows] = useState<Record<string | symbol, any>[]>([]); const [selectedRows, setSelectedRows] = useState<Record<string | symbol, any>[]>([]);
const onSelectChange = (newSelectedRowKeys: React.Key[], newSelectedRows: Record<string|symbol, any>[]) => { const onSelectChange = (
newSelectedRowKeys: React.Key[],
newSelectedRows: Record<string | symbol, any>[],
) => {
const keys = newSelectedRows.map((i: any) => i[safeKey] || i[key]); const keys = newSelectedRows.map((i: any) => i[safeKey] || i[key]);
setSelectedRowKeys(newSelectedRowKeys); setSelectedRowKeys(newSelectedRowKeys);
if (rowType === 'id') { if (rowType === 'id') {
@@ -38,11 +41,7 @@ export function useTableRowSelection(options: Partial<rowSelectionOptions> = {})
const rowSelection: TableRowSelection<Record<string, any>> = { const rowSelection: TableRowSelection<Record<string, any>> = {
selectedRowKeys, selectedRowKeys,
onChange: onSelectChange, onChange: onSelectChange,
selections: [ selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE],
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
],
}; };
return { rowSelection, selectedRowIDs, selectedRows, rowReset }; return { rowSelection, selectedRowIDs, selectedRows, rowReset };

25
src/icons/SplitIcon.tsx vendored Normal file
View File

@@ -0,0 +1,25 @@
import Icon from '@ant-design/icons';
import type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';
interface IconProps {
onClick: () => void;
}
export default function SplitIcon(props: Partial<CustomIconComponentProps & IconProps>) {
return (
<Icon
{...props}
component={() => (
<svg
className="chatico"
viewBox="0 0 1024 1024"
width="1em"
height="1em"
fill="currentColor"
>
<path d="M252.068571 906.496h520.283429c89.581714 0 134.144-44.562286 134.144-132.845714V250.331429c0-88.283429-44.562286-132.845714-134.144-132.845715H252.068571c-89.142857 0-134.582857 44.141714-134.582857 132.845715V773.668571c0 88.704 45.44 132.845714 134.582857 132.845715z m1.28-68.992c-42.843429 0-66.852571-22.710857-66.852571-67.291429V253.805714c0-44.580571 24.009143-67.291429 66.852571-67.291428h222.866286v651.008z m517.723429-651.008c42.422857 0 66.432 22.710857 66.432 67.291429V770.194286c0 44.580571-24.009143 67.291429-66.432 67.291428H548.205714V186.496z" />
</svg>
)}
/>
);
}

View File

@@ -23,6 +23,10 @@
-webkit-user-select: none; -webkit-user-select: none;
} }
.ant-layout-sider-children {
overflow-y: auto;
}
.chat-container { .chat-container {
padding: 20px; padding: 20px;
overflow: hidden; overflow: hidden;

59
src/layout/index.tsx vendored
View File

@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd'; import { Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd';
import { SyncOutlined } from '@ant-design/icons'; import { SyncOutlined } from '@ant-design/icons';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
@@ -13,29 +13,47 @@ const { Content, Footer, Sider } = Layout;
export default function ChatLayout() { export default function ChatLayout() {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [isDashboard, setDashboard] = useState<any>(null);
const [appInfo, setAppInfo] = useState<Record<string, any>>({}); const [appInfo, setAppInfo] = useState<Record<string, any>>({});
const location = useLocation(); const location = useLocation();
const [menuKey, setMenuKey] = useState(location.pathname);
const go = useNavigate(); const go = useNavigate();
useEffect(() => {
if (location.search === '?type=control') {
go('/settings');
}
if (location.search === '?type=preview') {
go('/?type=preview');
}
setMenuKey(location.pathname);
setDashboard(location.pathname === '/');
}, [location.pathname]);
useInit(async () => { useInit(async () => {
setAppInfo({ setAppInfo({
appName: await getName(), appName: await getName(),
appVersion: await getVersion(), appVersion: await getVersion(),
appTheme: await invoke("get_theme"), appTheme: await invoke('get_theme'),
});
}); });
})
const checkAppUpdate = async () => { const checkAppUpdate = async () => {
await invoke('run_check_update', { silent: false, hasMsg: true }); await invoke('run_check_update', { silent: false, hasMsg: true });
} };
const isDark = appInfo.appTheme === "dark"; const isDark = appInfo.appTheme === 'dark';
if (isDashboard === null) return null;
return ( return (
<ConfigProvider theme={{ algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm }}> <ConfigProvider theme={{ algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm }}>
{isDashboard ? (
<Routes />
) : (
<Layout style={{ minHeight: '100vh' }} hasSider> <Layout style={{ minHeight: '100vh' }} hasSider>
<Sider <Sider
theme={isDark ? "dark" : "light"} theme={isDark ? 'dark' : 'light'}
collapsible collapsible
collapsed={collapsed} collapsed={collapsed}
onCollapse={(value) => setCollapsed(value)} onCollapse={(value) => setCollapsed(value)}
@@ -49,41 +67,52 @@ export default function ChatLayout() {
zIndex: 999, zIndex: 999,
}} }}
> >
<div className="chat-logo"><img src="/logo.png" /></div> <div className="chat-logo">
<img src="/logo.png" />
</div>
<div className="chat-info"> <div className="chat-info">
<Tag>{appInfo.appName}</Tag> <Tag>{appInfo.appName}</Tag>
<Tag> <Tag>
<span style={{ marginRight: 5 }}>{appInfo.appVersion}</span> <span style={{ marginRight: 5 }}>{appInfo.appVersion}</span>
<Tooltip title="click to check update"> <Tooltip title="click to check update">
<a onClick={checkAppUpdate}><SyncOutlined /></a> <a onClick={checkAppUpdate}>
<SyncOutlined />
</a>
</Tooltip> </Tooltip>
</Tag> </Tag>
</div> </div>
<Menu <Menu
defaultSelectedKeys={[location.pathname]} selectedKeys={[menuKey]}
mode="inline" mode="inline"
theme={ appInfo.appTheme === "dark" ? "dark" : "light" } theme={appInfo.appTheme === 'dark' ? 'dark' : 'light'}
inlineIndent={12} inlineIndent={12}
items={menuItems} items={menuItems}
defaultOpenKeys={['/model']} // defaultOpenKeys={['/model']}
onClick={(i) => go(i.key)} onClick={(i) => go(i.key)}
/> />
</Sider> </Sider>
<Layout className="chat-layout" style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}> <Layout
className="chat-layout"
style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}
>
<Content <Content
className="chat-container" className="chat-container"
style={{ style={{
overflow: 'inherit' overflow: 'inherit',
}} }}
> >
<Routes /> <Routes />
</Content> </Content>
<Footer style={{ textAlign: 'center' }}> <Footer style={{ textAlign: 'center' }}>
<a href="https://github.com/lencx/chatgpt" target="_blank">ChatGPT Desktop Application</a> ©2022 Created by lencx <a href="https://github.com/lencx/chatgpt" target="_blank">
ChatGPT Desktop Application
</a>{' '}
©2022 Created by lencx
</Footer> </Footer>
</Layout> </Layout>
</Layout> </Layout>
)}
</ConfigProvider> </ConfigProvider>
); );
}; }

23
src/main.scss vendored
View File

@@ -14,11 +14,15 @@
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
} }
html, body { html,
body,
#root {
padding: 0; padding: 0;
margin: 0; margin: 0;
height: 100%;
} }
.ant-table-selection-col,
.ant-table-selection-column { .ant-table-selection-column {
width: 50px !important; width: 50px !important;
min-width: 50px !important; min-width: 50px !important;
@@ -56,6 +60,7 @@ html, body {
margin-bottom: 5px; margin-bottom: 5px;
} }
.chat-tags,
.chat-prompts-tags { .chat-prompts-tags {
.ant-tag { .ant-tag {
margin: 2px; margin: 2px;
@@ -68,12 +73,11 @@ html, body {
} }
} }
.chat-file-path, .chat-file-path {
.chat-sync-path {
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: #888; color: #888;
margin-bottom: 5px; margin-bottom: 3px;
line-height: 16px; line-height: 16px;
> div { > div {
@@ -81,7 +85,6 @@ html, body {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
// color: #2a2a2a;
} }
span { span {
@@ -96,3 +99,13 @@ html, body {
text-decoration: underline; text-decoration: underline;
} }
} }
.chatico {
cursor: pointer;
}
.awesome-tips {
.ant-tag {
cursor: pointer;
}
}

2
src/main.tsx vendored
View File

@@ -12,5 +12,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<Layout /> <Layout />
</BrowserRouter> </BrowserRouter>
</Suspense> </Suspense>
</StrictMode> </StrictMode>,
); );

48
src/routes.tsx vendored
View File

@@ -7,20 +7,26 @@ import {
UserOutlined, UserOutlined,
DownloadOutlined, DownloadOutlined,
FormOutlined, FormOutlined,
GlobalOutlined,
InfoCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import General from '@view/General'; import Settings from '@/view/settings';
import About from '@/view/about';
import Awesome from '@/view/awesome';
import UserCustom from '@/view/model/UserCustom'; import UserCustom from '@/view/model/UserCustom';
import SyncPrompts from '@/view/model/SyncPrompts'; import SyncPrompts from '@/view/model/SyncPrompts';
import SyncCustom from '@/view/model/SyncCustom'; import SyncCustom from '@/view/model/SyncCustom';
import SyncRecord from '@/view/model/SyncRecord'; import SyncRecord from '@/view/model/SyncRecord';
import Download from '@/view/download'; import Download from '@/view/download';
import Notes from '@/view/notes'; import Notes from '@/view/notes';
import Markdown from '@/view/markdown';
import Dashboard from '@/view/dashboard';
export type ChatRouteMetaObject = { export type ChatRouteMetaObject = {
label: string; label: string;
icon?: React.ReactNode, icon?: React.ReactNode;
}; };
type ChatRouteObject = { type ChatRouteObject = {
@@ -29,17 +35,25 @@ type ChatRouteObject = {
hideMenu?: boolean; hideMenu?: boolean;
meta?: ChatRouteMetaObject; meta?: ChatRouteMetaObject;
children?: ChatRouteObject[]; children?: ChatRouteObject[];
} };
export const routes: Array<ChatRouteObject> = [ export const routes: Array<ChatRouteObject> = [
{ {
path: '/', path: '/settings',
element: <General />, element: <Settings />,
meta: { meta: {
label: 'General', label: 'Settings',
icon: <SettingOutlined />, icon: <SettingOutlined />,
}, },
}, },
{
path: '/awesome',
element: <Awesome />,
meta: {
label: 'Awesome',
icon: <GlobalOutlined />,
},
},
{ {
path: '/notes', path: '/notes',
element: <Notes />, element: <Notes />,
@@ -48,6 +62,11 @@ export const routes: Array<ChatRouteObject> = [
icon: <FormOutlined />, icon: <FormOutlined />,
}, },
}, },
{
path: '/md/:id',
element: <Markdown />,
hideMenu: true,
},
{ {
path: '/model', path: '/model',
meta: { meta: {
@@ -88,19 +107,32 @@ export const routes: Array<ChatRouteObject> = [
], ],
}, },
{ {
path: 'download', path: '/download',
element: <Download />, element: <Download />,
meta: { meta: {
label: 'Download', label: 'Download',
icon: <DownloadOutlined />, icon: <DownloadOutlined />,
}, },
}, },
{
path: '/about',
element: <About />,
meta: {
label: 'About',
icon: <InfoCircleOutlined />,
},
},
{
path: '/',
element: <Dashboard />,
hideMenu: true,
},
]; ];
type MenuItem = Required<MenuProps>['items'][number]; type MenuItem = Required<MenuProps>['items'][number];
export const menuItems: MenuItem[] = routes export const menuItems: MenuItem[] = routes
.filter((j) => !j.hideMenu) .filter((j) => !j.hideMenu)
.map(i => ({ .map((i) => ({
...i.meta, ...i.meta,
key: i.path || '', key: i.path || '',
children: i?.children children: i?.children

56
src/utils.ts vendored
View File

@@ -2,31 +2,36 @@ import { readTextFile, writeTextFile, exists, createDir } from '@tauri-apps/api/
import { homeDir, join, dirname } from '@tauri-apps/api/path'; import { homeDir, join, dirname } from '@tauri-apps/api/path';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
export const APP_CONF_JSON = 'chat.conf.json';
export const CHAT_MODEL_JSON = 'chat.model.json'; export const CHAT_MODEL_JSON = 'chat.model.json';
export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json'; export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json';
export const CHAT_DOWNLOAD_JSON = 'chat.download.json'; export const CHAT_DOWNLOAD_JSON = 'chat.download.json';
export const CHAT_AWESOME_JSON = 'chat.awesome.json';
export const CHAT_NOTES_JSON = 'chat.notes.json'; export const CHAT_NOTES_JSON = 'chat.notes.json';
export const CHAT_PROMPTS_CSV = 'chat.prompts.csv'; export const CHAT_PROMPTS_CSV = 'chat.prompts.csv';
export const GITHUB_PROMPTS_CSV_URL = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv'; export const GITHUB_PROMPTS_CSV_URL =
'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv';
export const GITHUB_LOG_URL = 'https://raw.githubusercontent.com/lencx/ChatGPT/main/UPDATE_LOG.md';
export const DISABLE_AUTO_COMPLETE = { export const DISABLE_AUTO_COMPLETE = {
autoCapitalize: 'off', autoCapitalize: 'off',
autoComplete: 'off', autoComplete: 'off',
spellCheck: false spellCheck: false,
}; };
export const chatRoot = async () => { export const chatRoot = async () => {
return join(await homeDir(), '.chatgpt') return join(await homeDir(), '.chatgpt');
} };
export const chatModelPath = async (): Promise<string> => { export const chatModelPath = async (): Promise<string> => {
return join(await chatRoot(), CHAT_MODEL_JSON); return join(await chatRoot(), CHAT_MODEL_JSON);
} };
export const chatPromptsPath = async (): Promise<string> => { export const chatPromptsPath = async (): Promise<string> => {
return join(await chatRoot(), CHAT_PROMPTS_CSV); return join(await chatRoot(), CHAT_PROMPTS_CSV);
} };
type readJSONOpts = { defaultVal?: Record<string, any>, isRoot?: boolean, isList?: boolean }; type readJSONOpts = { defaultVal?: Record<string, any>; isRoot?: boolean; isList?: boolean };
export const readJSON = async (path: string, opts: readJSONOpts = {}) => { export const readJSON = async (path: string, opts: readJSONOpts = {}) => {
const { defaultVal = {}, isRoot = false, isList = false } = opts; const { defaultVal = {}, isRoot = false, isList = false } = opts;
const root = await chatRoot(); const root = await chatRoot();
@@ -36,15 +41,24 @@ export const readJSON = async (path: string, opts: readJSONOpts = {}) => {
file = await join(root, path); file = await join(root, path);
} }
if (!await exists(file)) { if (!(await exists(file))) {
if (await dirname(file) !== root) { if ((await dirname(file)) !== root) {
await createDir(await dirname(file), { recursive: true }); await createDir(await dirname(file), { recursive: true });
} }
await writeTextFile(file, isList ? '[]' : JSON.stringify({ await writeTextFile(
file,
isList
? '[]'
: JSON.stringify(
{
name: 'ChatGPT', name: 'ChatGPT',
link: 'https://github.com/lencx/ChatGPT', link: 'https://github.com/lencx/ChatGPT',
...defaultVal, ...defaultVal,
}, null, 2)) },
null,
2,
),
);
} }
try { try {
@@ -52,10 +66,14 @@ export const readJSON = async (path: string, opts: readJSONOpts = {}) => {
} catch (e) { } catch (e) {
return {}; return {};
} }
} };
type writeJSONOpts = { dir?: string, isRoot?: boolean }; type writeJSONOpts = { dir?: string; isRoot?: boolean };
export const writeJSON = async (path: string, data: Record<string, any>, opts: writeJSONOpts = {}) => { export const writeJSON = async (
path: string,
data: Record<string, any>,
opts: writeJSONOpts = {},
) => {
const { isRoot = false } = opts; const { isRoot = false } = opts;
const root = await chatRoot(); const root = await chatRoot();
let file = path; let file = path;
@@ -64,13 +82,17 @@ export const writeJSON = async (path: string, data: Record<string, any>, opts: w
file = await join(root, path); file = await join(root, path);
} }
if (isRoot && !await exists(await dirname(file))) { if (isRoot && !(await exists(await dirname(file)))) {
await createDir(await dirname(file), { recursive: true }); await createDir(await dirname(file), { recursive: true });
} }
await writeTextFile(file, JSON.stringify(data, null, 2)); await writeTextFile(file, JSON.stringify(data, null, 2));
} };
export const fmtDate = (date: any) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'); export const fmtDate = (date: any) => dayjs(date).format('YYYY-MM-DD HH:mm:ss');
export const genCmd = (act: string) => act.replace(/\s+|\/+/g, '_').replace(/[^\d\w]/g, '').toLocaleLowerCase(); export const genCmd = (act: string) =>
act
.replace(/\s+|\/+/g, '_')
.replace(/[^\d\w]/g, '')
.toLocaleLowerCase();

182
src/view/General.tsx vendored
View File

@@ -1,182 +0,0 @@
import { useEffect, useState } from 'react';
import { Form, Radio, Switch, Input, Button, Space, message, Tooltip } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { invoke, shell, path } from '@tauri-apps/api';
import { platform } from '@tauri-apps/api/os';
import { ask } from '@tauri-apps/api/dialog';
import { relaunch } from '@tauri-apps/api/process';
import { clone, omit, isEqual } from 'lodash';
import useInit from '@/hooks/useInit';
import { DISABLE_AUTO_COMPLETE, chatRoot } from '@/utils';
const AutoUpdateLabel = () => {
return (
<span>
Auto Update <Tooltip title={(
<div>
<div>Auto Update Policy</div>
<span><strong>Prompt</strong>: prompt to install</span><br/>
<span><strong>Silent</strong>: install silently</span><br/>
{/*<span><strong>Disable</strong>: disable auto update</span><br/>*/}
</div>
)}><QuestionCircleOutlined style={{ color: '#1677ff' }} /></Tooltip>
</span>
)
}
const OriginLabel = ({ url }: { url: string }) => {
return (
<span>
Switch Origin <Tooltip title={`Default: ${url}`}><QuestionCircleOutlined style={{ color: '#1677ff' }} /></Tooltip>
</span>
)
}
const PopupSearchLabel = () => {
return (
<span>
Pop-up Search
{' '}
<Tooltip title={(
<div>
<div style={{ marginBottom: 10 }}>Generate images according to the content: Select the ChatGPT content with the mouse, no more than 400 characters. the <b>DALL·E 2</b> button appears, and click to jump (Note: because the search content filled by the script cannot trigger the event directly, you need to enter a space in the input box to make the button clickable).</div>
<div>The application is built using Tauri, and due to its security restrictions, some of the action buttons will not work, so we recommend going to your browser.</div>
</div>
)}><QuestionCircleOutlined style={{ color: '#1677ff' }} /></Tooltip>
</span>
)
}
const GlobalShortcutLabel = () => {
return (
<div>
Global Shortcut
{' '}
<Tooltip title={(
<div>
<div>Shortcut definition, modifiers and key separated by "+" e.g. CmdOrControl+Q</div>
<div style={{ margin: '10px 0'}}>If empty, the shortcut is disabled.</div>
<a href="https://tauri.app/v1/api/js/globalshortcut" target="_blank">https://tauri.app/v1/api/js/globalshortcut</a>
</div>
)}>
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
</Tooltip>
</div>
)
}
export default function General() {
const [form] = Form.useForm();
const [jsonPath, setJsonPath] = useState('');
const [platformInfo, setPlatform] = useState<string>('');
const [chatConf, setChatConf] = useState<any>(null);
useInit(async () => {
setJsonPath(await path.join(await chatRoot(), 'chat.conf.json'));
setPlatform(await platform());
const chatData = await invoke('get_chat_conf');
setChatConf(chatData);
});
useEffect(() => {
form.setFieldsValue(clone(chatConf));
}, [chatConf])
const onCancel = () => {
form.setFieldsValue(chatConf);
};
const onReset = async () => {
const chatData = await invoke('reset_chat_conf');
setChatConf(chatData);
const isOk = await ask(`Configuration reset successfully, whether to restart?`, {
title: 'ChatGPT Preferences'
});
if (isOk) {
relaunch();
return;
}
message.success('Configuration reset successfully');
};
const onFinish = async (values: any) => {
if (!isEqual(omit(chatConf, ['default_origin']), values)) {
await invoke('form_confirm', { data: values, label: 'main' });
const isOk = await ask(`Configuration saved successfully, whether to restart?`, {
title: 'ChatGPT Preferences'
});
if (isOk) {
relaunch();
return;
}
message.success('Configuration saved successfully');
}
};
return (
<>
<div className="chat-table-tip">
<div className="chat-sync-path">
<div>PATH: <a onClick={() => shell.open(jsonPath)} title={jsonPath}>{jsonPath}</a></div>
</div>
</div>
<Form
form={form}
style={{ maxWidth: 500 }}
onFinish={onFinish}
labelCol={{ span: 8 }}
wrapperCol={{ span: 15, offset: 1 }}
>
<Form.Item label="Stay On Top" name="stay_on_top" valuePropName="checked">
<Switch />
</Form.Item>
{platformInfo === 'darwin' && (
<Form.Item label="Titlebar" name="titlebar" valuePropName="checked">
<Switch />
</Form.Item>
)}
<Form.Item label={<PopupSearchLabel />} name="popup_search" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="Theme" name="theme">
<Radio.Group>
<Radio value="Light">Light</Radio>
<Radio value="Dark">Dark</Radio>
{["darwin", "windows"].includes(platformInfo) && (
<Radio value="System">System</Radio>
)}
</Radio.Group>
</Form.Item>
<Form.Item label={<AutoUpdateLabel />} name="auto_update">
<Radio.Group>
<Radio value="Prompt">Prompt</Radio>
<Radio value="Silent">Silent</Radio>
{/*<Radio value="Disable">Disable</Radio>*/}
</Radio.Group>
</Form.Item>
<Form.Item label={<GlobalShortcutLabel />} name="global_shortcut">
<Input placeholder="CmdOrCtrl+Shift+O" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
<Form.Item label={<OriginLabel url={chatConf?.default_origin} />} name="origin">
<Input placeholder="https://chat.openai.com" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
<Form.Item label="User Agent (Window)" name="ua_window">
<Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...DISABLE_AUTO_COMPLETE} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" />
</Form.Item>
<Form.Item label="User Agent (SystemTray)" name="ua_tray">
<Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...DISABLE_AUTO_COMPLETE} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" />
</Form.Item>
<Form.Item>
<Space size={20}>
<Button onClick={onCancel}>Cancel</Button>
<Button type="primary" htmlType="submit">Submit</Button>
<Button type="dashed" onClick={onReset}>Reset to defaults</Button>
</Space>
</Form.Item>
</Form>
</>
)
}

28
src/view/about/index.scss vendored Normal file
View File

@@ -0,0 +1,28 @@
.about {
.log-tab {
font-size: 14px;
h2 {
font-size: 16px;
}
}
.markdown-body {
background-color: unset;
}
.about-tab {
.imgs {
img {
max-width: 200px;
margin-bottom: 20px;
}
}
}
code {
background-color: rgba(200, 200, 200, 0.4);
padding: 2px 4px;
border-radius: 5px;
}
}

88
src/view/about/index.tsx vendored Normal file
View File

@@ -0,0 +1,88 @@
import { useState } from 'react';
import { invoke } from '@tauri-apps/api';
import { Tabs, Tag } from 'antd';
import { GITHUB_LOG_URL } from '@/utils';
import useInit from '@/hooks/useInit';
import Markdown from '@/components/Markdown';
import './index.scss';
export default function About() {
const [logContent, setLogContent] = useState('');
useInit(async () => {
const data = (await invoke('get_data', { url: GITHUB_LOG_URL })) || '';
setLogContent(data as string);
});
return (
<div className="about">
<Tabs
items={[
{ label: 'About ChatGPT', key: 'about', children: <AboutChatGPT /> },
{ label: 'Update Log', key: 'log', children: <LogTab content={logContent} /> },
]}
/>
</div>
);
}
const AboutChatGPT = () => {
return (
<div className="about-tab">
<Tag>ChatGPT Desktop Application (Mac, Windows and Linux)</Tag>
<p>
🕒 History versions:{' '}
<a href="https://github.com/lencx/ChatGPT/releases" target="_blank">
lencx/ChatGPT/releases
</a>
</p>
<p>
It is just a wrapper for the
<a href="https://chat.openai.com" target="_blank" title="https://chat.openai.com">
{' '}
OpenAI ChatGPT{' '}
</a>
website, no other data transfer exists (you can check the{' '}
<a
href="https://github.com/lencx/ChatGPT"
target="_blank"
title="https://github.com/lencx/ChatGPT"
>
{' '}
source code{' '}
</a>
). The development and maintenance of this software has taken up a lot of my time. If it
helps you, you can buy me a cup of coffee (Chinese users can use WeChat to scan the code),
thanks!
</p>
<p className="imgs">
<a href="https://www.buymeacoffee.com/lencx" target="_blank">
<img
src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png"
alt="Buy Me A Coffee"
/>
</a>{' '}
<br />
<img
width="200"
src="https://user-images.githubusercontent.com/16164244/207228025-117b5f77-c5d2-48c2-a070-774b7a1596f2.png"
></img>
</p>
</div>
);
};
const LogTab = ({ content }: { content: string }) => {
return (
<div>
<p>
Ref:{' '}
<a href="https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md" target="_blank">
lencx/ChatGPT/UPDATE_LOG.md
</a>
</p>
<Markdown className="log-tab" children={content} />
</div>
);
};

63
src/view/awesome/Form.tsx vendored Normal file
View File

@@ -0,0 +1,63 @@
import { useEffect, ForwardRefRenderFunction, useImperativeHandle, forwardRef } from 'react';
import { Form, Input, Switch } from 'antd';
import type { FormProps } from 'antd';
import Tags from '@comps/Tags';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
interface AwesomeFormProps {
record?: Record<string | symbol, any> | null;
}
const initFormValue = {
title: '',
url: '',
enable: true,
tags: [],
category: '',
};
const AwesomeForm: ForwardRefRenderFunction<FormProps, AwesomeFormProps> = ({ record }, ref) => {
const [form] = Form.useForm();
useImperativeHandle(ref, () => ({ form }));
useEffect(() => {
if (record) {
form.setFieldsValue(record);
}
}, [record]);
return (
<Form form={form} labelCol={{ span: 4 }} initialValues={initFormValue}>
<Form.Item
label="Title"
name="title"
rules={[{ required: true, message: 'Please enter a title!' }]}
>
<Input placeholder="Please enter a title" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
<Form.Item
label="URL"
name="url"
rules={[{ required: true, message: 'Please enter the URL' }, { type: 'url' }]}
>
<Input placeholder="Please enter the URL" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
<Form.Item
label="Category"
name="category"
rules={[{ required: true, message: 'Please enter a category' }]}
>
<Input placeholder="Please enter a category" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
<Form.Item label="Tags" name="tags">
<Tags value={record?.tags} />
</Form.Item>
<Form.Item label="Enable" name="enable" valuePropName="checked">
<Switch />
</Form.Item>
</Form>
);
};
export default forwardRef(AwesomeForm);

74
src/view/awesome/config.tsx vendored Normal file
View File

@@ -0,0 +1,74 @@
import { Tag, Space, Popconfirm, Switch } from 'antd';
import { open } from '@tauri-apps/api/shell';
export const awesomeColumns = () => [
{
title: 'Title',
dataIndex: 'title',
fixed: 'left',
key: 'title',
width: 160,
},
{
title: 'URL',
dataIndex: 'url',
key: 'url',
width: 200,
render: (v: string) => <a onClick={() => open(v)}>{v}</a>,
},
// {
// title: 'Icon',
// dataIndex: 'icon',
// key: 'icon',
// width: 120,
// },
{
title: 'Enable',
dataIndex: 'enable',
key: 'enable',
width: 80,
render: (v: boolean = true, row: Record<string, any>, action: Record<string, any>) => (
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
),
},
{
title: 'Category',
dataIndex: 'category',
key: 'category',
width: 120,
render: (v: string) => <Tag color="geekblue">{v}</Tag>,
},
{
title: 'Tags',
dataIndex: 'tags',
key: 'tags',
width: 150,
render: (v: string[]) => (
<span className="chat-tags">
{v?.map((i) => (
<Tag key={i}>{i}</Tag>
))}
</span>
),
},
{
title: 'Action',
fixed: 'right',
width: 150,
render: (_: any, row: any, actions: any) => {
return (
<Space>
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
<Popconfirm
title="Are you sure you want to delete this URL?"
onConfirm={() => actions.setRecord(row, 'delete')}
okText="Yes"
cancelText="No"
>
<a>Delete</a>
</Popconfirm>
</Space>
);
},
},
];

197
src/view/awesome/index.tsx vendored Normal file
View File

@@ -0,0 +1,197 @@
import { useRef, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Table, Modal, Popconfirm, Button, Tooltip, Tag, message } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { invoke } from '@tauri-apps/api';
import useJson from '@/hooks/useJson';
import useData from '@/hooks/useData';
import useColumns from '@/hooks/useColumns';
import FilePath from '@/components/FilePath';
import { CHAT_AWESOME_JSON } from '@/utils';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { awesomeColumns } from './config';
import AwesomeForm from './Form';
export default function Awesome() {
const formRef = useRef<any>(null);
const [isVisible, setVisible] = useState(false);
const { opData, opInit, opAdd, opReplace, opReplaceItems, opRemove, opRemoveItems, opSafeKey } =
useData([]);
const { columns, ...opInfo } = useColumns(awesomeColumns());
const { rowSelection, selectedRowIDs, rowReset } = useTableRowSelection();
const { json, updateJson } = useJson<any[]>(CHAT_AWESOME_JSON);
const selectedItems = rowSelection.selectedRowKeys || [];
useEffect(() => {
if (!json || json.length <= 0) return;
opInit(json);
}, [json?.length]);
useEffect(() => {
if (!opInfo.opType) return;
if (['edit', 'new'].includes(opInfo.opType)) {
setVisible(true);
}
if (['delete'].includes(opInfo.opType)) {
const data = opRemove(opInfo?.opRecord?.[opSafeKey]);
updateJson(data);
opInfo.resetRecord();
}
}, [opInfo.opType, formRef]);
const hide = () => {
setVisible(false);
opInfo.resetRecord();
};
useEffect(() => {
if (opInfo.opType === 'enable') {
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
updateJson(data);
}
}, [opInfo.opTime]);
const handleDelete = () => {
const data = opRemoveItems(selectedRowIDs);
updateJson(data);
rowReset();
message.success('All selected URLs have been deleted');
};
const handleOk = () => {
formRef.current?.form?.validateFields().then(async (vals: Record<string, any>) => {
let idx = opData.findIndex((i) => i.url === vals.url);
if (vals.url === opInfo?.opRecord?.url) {
idx = -1;
}
if (idx === -1) {
if (opInfo.opType === 'new') {
const data = opAdd(vals);
await updateJson(data);
opInit(data);
message.success('Data added successfully');
}
if (opInfo.opType === 'edit') {
const data = opReplace(opInfo?.opRecord?.[opSafeKey], vals);
await updateJson(data);
message.success('Data updated successfully');
}
hide();
} else {
const data = opData[idx];
message.error(
<div style={{ width: 360 }}>
<div>
<b>
{data.title}: {data.url}
</b>
</div>
<div>This URL already exists, please edit it and try again.</div>
</div>,
);
}
});
};
const handleEnable = (isEnable: boolean) => {
const data = opReplaceItems(selectedRowIDs, { enable: isEnable });
updateJson(data);
};
const handlePreview = () => {
invoke('wa_window', {
label: 'awesome_preview',
url: 'index.html?type=preview',
title: 'Preview Dashboard',
});
};
const modalTitle = `${{ new: 'Create', edit: 'Edit' }[opInfo.opType]} URL`;
return (
<div>
<div className="chat-table-btns">
<div>
<Button className="chat-add-btn" type="primary" onClick={opInfo.opNew}>
Add URL
</Button>
<Button type="dashed" onClick={handlePreview}>
Preview Dashboard
</Button>
<PreviewTip />
</div>
<div>
{selectedItems.length > 0 && (
<>
<Button type="primary" onClick={() => handleEnable(true)}>
Enable
</Button>
<Button onClick={() => handleEnable(false)}>Disable</Button>
<Popconfirm
overlayStyle={{ width: 250 }}
title="URLs cannot be recovered after deletion, are you sure you want to delete them?"
placement="topLeft"
onConfirm={handleDelete}
okText="Yes"
cancelText="No"
>
<Button>Delete</Button>
</Popconfirm>
<span className="num">Selected {selectedItems.length} items</span>
</>
)}
</div>
</div>
<FilePath paths={CHAT_AWESOME_JSON} />
<Table
rowKey="url"
columns={columns}
scroll={{ x: 800 }}
dataSource={opData}
rowSelection={rowSelection}
pagination={TABLE_PAGINATION}
/>
<Modal
open={isVisible}
title={modalTitle}
onCancel={hide}
onOk={handleOk}
destroyOnClose
maskClosable={false}
>
<AwesomeForm ref={formRef} record={opInfo?.opRecord} />
</Modal>
</div>
);
}
const PreviewTip = () => {
const go = useNavigate();
const handleGo = (v: string) => {
go(`/settings?type=${v}`);
};
return (
<Tooltip
overlayInnerStyle={{ width: 400 }}
title={
<div className="awesome-tips">
Click the button to preview, and in
<Link to="/settings"> Settings </Link>
you can set a single URL or Dashboard as the default window for the app.
<br />
<Tag onClick={() => handleGo('main_window')} color="blue">
Main Window
</Tag>
{'or '}
<Tag onClick={() => handleGo('tray_window')} color="blue">
SystemTray Window
</Tag>
</div>
}
>
<QuestionCircleOutlined style={{ marginLeft: 5, color: '#1677ff' }} />
</Tooltip>
);
};

73
src/view/dashboard/index.scss vendored Normal file
View File

@@ -0,0 +1,73 @@
.dashboard {
position: fixed;
width: calc(100% - 30px);
height: calc(100% - 30px);
overflow-y: auto;
padding: 15px;
&-no-data {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
flex-direction: column;
font-weight: bold;
.icon {
color: #989898;
font-size: 24px;
}
.txt {
padding: 10px;
font-size: 12px;
line-height: 16px;
a {
color: #1677ff;
cursor: pointer;
}
}
}
&.dark {
background-color: #000;
}
&.has-top-dom {
padding-top: 30px;
}
&.preview {
padding-top: 15px;
}
.group-item {
margin-bottom: 20px;
.title {
font-weight: bold;
font-size: 18px;
margin-bottom: 10px;
}
.item {
.ant-card-body {
padding: 10px;
text-align: center;
font-weight: 500;
font-size: 14px;
}
span {
display: block;
height: 100%;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}

108
src/view/dashboard/index.tsx vendored Normal file
View File

@@ -0,0 +1,108 @@
import { useEffect, useState } from 'react';
import clsx from 'clsx';
import { useSearchParams } from 'react-router-dom';
import { Row, Col, Card } from 'antd';
import { InboxOutlined } from '@ant-design/icons';
import { os, invoke } from '@tauri-apps/api';
import useInit from '@/hooks/useInit';
import useJson from '@/hooks/useJson';
import { CHAT_AWESOME_JSON, APP_CONF_JSON, readJSON } from '@/utils';
import './index.scss';
export default function Dashboard() {
const [params] = useSearchParams();
const { json } = useJson<Record<string, any>[]>(CHAT_AWESOME_JSON);
const [list, setList] = useState<Array<[string, Record<string, any>[]]>>();
const [hasClass, setClass] = useState(false);
const [theme, setTheme] = useState('');
useInit(async () => {
const getOS = await os.platform();
const conf = await readJSON(APP_CONF_JSON);
const appTheme = await invoke('get_theme');
setTheme(appTheme as string);
setClass(!conf?.titlebar && getOS === 'darwin');
});
useEffect(() => {
if (!json) return;
const categories = new Map();
if (Array.isArray(json)) {
json?.forEach((i) => {
if (!i.enable) return;
if (!categories.has(i.category)) {
categories.set(i.category, []);
}
categories.get(i?.category).push(i);
});
setList(Array.from(categories) || []);
} else {
setList([]);
}
}, [JSON.stringify(json)]);
const handleLink = async (item: Record<string, any>) => {
await invoke('wa_window', {
label: btoa(item.url).replace(/[^a-zA-Z0-9]/g, ''),
title: item.title,
url: item.url,
});
};
if (!list) return null;
if (list?.length === 0) {
return (
<div className="dashboard-no-data">
<div className="icon">
<InboxOutlined style={{ fontSize: 80, marginBottom: 5 }} />
<br />
No data
</div>
<div className="txt">
Go to <a onClick={() => invoke('control_window')}>{'Control Center -> Awesome'}</a> to add
data and make sure they are enabled.
</div>
</div>
);
}
return (
<div
className={clsx('dashboard', theme, {
'has-top-dom': hasClass,
preview: params.get('type') === 'preview',
})}
>
<div>
{list.map((i) => {
return (
<div key={i[0]} className="group-item">
<Card title={i[0]} size="small">
<Row className="list" gutter={[8, 8]}>
{i[1].map((j, idx) => {
return (
<Col
title={`${j?.title}: ${j?.url}`}
key={`${idx}_${j?.url}`}
xl={4}
md={6}
sm={8}
xs={12}
>
<Card className="item" hoverable onClick={() => handleLink(j)}>
<span>{j?.title}</span>
</Card>
</Col>
);
})}
</Row>
</Card>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -10,7 +10,7 @@ import { fmtDate, chatRoot } from '@/utils';
const colorMap: any = { const colorMap: any = {
pdf: 'blue', pdf: 'blue',
png: 'orange', png: 'orange',
} };
export const downloadColumns = () => [ export const downloadColumns = () => [
{ {
@@ -61,20 +61,22 @@ export const downloadColumns = () => [
<a>Delete</a> <a>Delete</a>
</Popconfirm> </Popconfirm>
</Space> </Space>
) );
} },
} },
]; ];
const RenderPath = ({ row }: any) => { const RenderPath = ({ row }: any) => {
const [filePath, setFilePath] = useState(''); const [filePath, setFilePath] = useState('');
useInit(async () => { useInit(async () => {
setFilePath(await getPath(row)); setFilePath(await getPath(row));
}) });
return <a onClick={() => shell.open(filePath)}>{filePath}</a>; return <a onClick={() => shell.open(filePath)}>{filePath}</a>;
}; };
export const getPath = async (row: any) => { export const getPath = async (row: any) => {
const isImg = ['png'].includes(row?.ext); const isImg = ['png'].includes(row?.ext);
return await path.join(await chatRoot(), 'download', isImg ? 'img' : row.ext, row.id) + `.${row.ext}`; return (
} (await path.join(await chatRoot(), 'download', isImg ? 'img' : row.ext, row.id)) + `.${row.ext}`
);
};

View File

@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Table, Modal, Popconfirm, Button, message } from 'antd'; import { Table, Modal, Popconfirm, Button, message } from 'antd';
import { invoke, path, shell, fs } from '@tauri-apps/api'; import { invoke, path, fs } from '@tauri-apps/api';
import useInit from '@/hooks/useInit';
import useJson from '@/hooks/useJson'; import useJson from '@/hooks/useJson';
import useData from '@/hooks/useData'; import useData from '@/hooks/useData';
import useColumns from '@/hooks/useColumns'; import useColumns from '@/hooks/useColumns';
import FilePath from '@/components/FilePath';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils'; import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils';
import { downloadColumns } from './config'; import { downloadColumns } from './config';
@@ -19,7 +19,6 @@ function renderFile(buff: Uint8Array, type: string) {
} }
export default function Download() { export default function Download() {
const [downloadPath, setDownloadPath] = useState('');
const [source, setSource] = useState(''); const [source, setSource] = useState('');
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const { opData, opInit, opReplace, opSafeKey } = useData([]); const { opData, opInit, opReplace, opSafeKey } = useData([]);
@@ -28,11 +27,6 @@ export default function Download() {
const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_DOWNLOAD_JSON); const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_DOWNLOAD_JSON);
const selectedItems = rowSelection.selectedRowKeys || []; const selectedItems = rowSelection.selectedRowKeys || [];
useInit(async () => {
const file = await path.join(await chatRoot(), CHAT_DOWNLOAD_JSON);
setDownloadPath(file);
});
useEffect(() => { useEffect(() => {
if (!json || json.length <= 0) return; if (!json || json.length <= 0) return;
opInit(json); opInit(json);
@@ -43,7 +37,12 @@ export default function Download() {
(async () => { (async () => {
const record = opInfo?.opRecord; const record = opInfo?.opRecord;
const isImg = ['png'].includes(record?.ext); const isImg = ['png'].includes(record?.ext);
const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : record?.ext, `${record?.id}.${record?.ext}`); const file = await path.join(
await chatRoot(),
'download',
isImg ? 'img' : record?.ext,
`${record?.id}.${record?.ext}`,
);
if (opInfo.opType === 'preview') { if (opInfo.opType === 'preview') {
const data = await fs.readBinaryFile(file); const data = await fs.readBinaryFile(file);
const sourceData = renderFile(data, record?.ext); const sourceData = renderFile(data, record?.ext);
@@ -61,8 +60,8 @@ export default function Download() {
message.success('Name has been changed!'); message.success('Name has been changed!');
} }
opInfo.resetRecord(); opInfo.resetRecord();
})() })();
}, [opInfo.opType]) }, [opInfo.opType]);
const handleDelete = async () => { const handleDelete = async () => {
if (opData?.length === selectedRows.length) { if (opData?.length === selectedRows.length) {
@@ -75,10 +74,15 @@ export default function Download() {
const rows = selectedRows.map(async (i) => { const rows = selectedRows.map(async (i) => {
const isImg = ['png'].includes(i?.ext); const isImg = ['png'].includes(i?.ext);
const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : i?.ext, `${i?.id}.${i?.ext}`); const file = await path.join(
await chatRoot(),
'download',
isImg ? 'img' : i?.ext,
`${i?.id}.${i?.ext}`,
);
await fs.removeFile(file); await fs.removeFile(file);
return file; return file;
}) });
Promise.all(rows).then(async () => { Promise.all(rows).then(async () => {
await handleRefresh(); await handleRefresh();
message.success('All files selected are cleared!'); message.success('All files selected are cleared!');
@@ -111,18 +115,14 @@ export default function Download() {
okText="Yes" okText="Yes"
cancelText="No" cancelText="No"
> >
<Button>Batch delete</Button> <Button>Delete</Button>
</Popconfirm> </Popconfirm>
<span className="num">Selected {selectedItems.length} items</span> <span className="num">Selected {selectedItems.length} items</span>
</> </>
)} )}
</div> </div>
</div> </div>
<div className="chat-table-tip"> <FilePath paths={CHAT_DOWNLOAD_JSON} />
<div className="chat-file-path">
<div>PATH: <a onClick={() => shell.open(downloadPath)} title={downloadPath}>{downloadPath}</a></div>
</div>
</div>
<Table <Table
rowKey="id" rowKey="id"
columns={columns} columns={columns}
@@ -141,5 +141,5 @@ export default function Download() {
<img style={{ maxWidth: '100%' }} src={source} /> <img style={{ maxWidth: '100%' }} src={source} />
</Modal> </Modal>
</div> </div>
) );
} }

16
src/view/markdown/index.scss vendored Normal file
View File

@@ -0,0 +1,16 @@
.md-task {
margin-bottom: 5px;
display: flex;
justify-content: space-between;
.ant-breadcrumb-link {
padding: 3px 5px;
transition: all 300ms ease;
border-radius: 4px;
&:hover {
color: rgba(0, 0, 0, 0.88);
background-color: rgba(0, 0, 0, 0.06);
cursor: pointer;
}
}
}

58
src/view/markdown/index.tsx vendored Normal file
View File

@@ -0,0 +1,58 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Breadcrumb } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import MarkdownEditor from '@/components/Markdown/Editor';
import { fs, shell } from '@tauri-apps/api';
import useInit from '@/hooks/useInit';
import SplitIcon from '@/icons/SplitIcon';
import { getPath } from '@/view/notes/config';
import './index.scss';
const modeMap: any = {
0: 'split',
1: 'md',
2: 'doc',
};
export default function Markdown() {
const [filePath, setFilePath] = useState('');
const [source, setSource] = useState('');
const [previewMode, setPreviewMode] = useState(0);
const location = useLocation();
const state = location?.state;
useInit(async () => {
const file = await getPath(state);
setFilePath(file);
setSource(await fs.readTextFile(file));
});
const handleChange = async (v: string) => {
await fs.writeTextFile(filePath, v);
};
const handlePreview = () => {
let mode = previewMode + 1;
if (mode > 2) mode = 0;
setPreviewMode(mode);
};
return (
<>
<div className="md-task">
<Breadcrumb separator="">
<Breadcrumb.Item onClick={() => history.go(-1)}>
<ArrowLeftOutlined />
</Breadcrumb.Item>
<Breadcrumb.Item onClick={() => shell.open(filePath)}>{filePath}</Breadcrumb.Item>
</Breadcrumb>
<div>
<SplitIcon onClick={handlePreview} style={{ fontSize: 18, color: 'rgba(0,0,0,0.5)' }} />
</div>
</div>
<MarkdownEditor value={source} onChange={handleChange} mode={modeMap[previewMode]} />
</>
);
}

View File

@@ -1,4 +1,10 @@
import { useEffect, useState, ForwardRefRenderFunction, useImperativeHandle, forwardRef } from 'react'; import {
useEffect,
useState,
ForwardRefRenderFunction,
useImperativeHandle,
forwardRef,
} from 'react';
import { Form, Input, Select, Tooltip } from 'antd'; import { Form, Input, Select, Tooltip } from 'antd';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import type { FormProps } from 'antd'; import type { FormProps } from 'antd';
@@ -54,10 +60,18 @@ const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record,
const jsonTip = ( const jsonTip = (
<Tooltip <Tooltip
title={<pre>{JSON.stringify([ title={
<pre>
{JSON.stringify(
[
{ cmd: '', act: '', prompt: '' }, { cmd: '', act: '', prompt: '' },
{ cmd: '', act: '', prompt: '' }, { cmd: '', act: '', prompt: '' },
], null, 2)}</pre>} ],
null,
2,
)}
</pre>
}
> >
<a>JSON</a> <a>JSON</a>
</Tooltip> </Tooltip>
@@ -65,10 +79,12 @@ const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record,
const csvTip = ( const csvTip = (
<Tooltip <Tooltip
title={<pre>{`"cmd","act","prompt" title={
<pre>{`"cmd","act","prompt"
"cmd","act","prompt" "cmd","act","prompt"
"cmd","act","prompt" "cmd","act","prompt"
"cmd","act","prompt"`}</pre>} "cmd","act","prompt"`}</pre>
}
> >
<a>CSV</a> <a>CSV</a>
</Tooltip> </Tooltip>
@@ -76,22 +92,18 @@ const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record,
return ( return (
<> <>
<Form <Form form={form} labelCol={{ span: 4 }} initialValues={initFormValue}>
form={form}
labelCol={{ span: 4 }}
initialValues={initFormValue}
>
<Form.Item <Form.Item
label="Name" label="Name"
name="name" name="name"
rules={[{ required: true, message: 'Please input name!' }]} rules={[{ required: true, message: 'Please enter a name!' }]}
> >
<Input placeholder="Please input name" {...DISABLE_AUTO_COMPLETE} /> <Input placeholder="Please enter a name" {...DISABLE_AUTO_COMPLETE} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="PATH" label="PATH"
name="path" name="path"
rules={[{ required: true, message: 'Please input path!' }]} rules={[{ required: true, message: 'Please enter the path!' }]}
> >
<Input <Input
placeholder="YOUR_PATH" placeholder="YOUR_PATH"
@@ -100,13 +112,17 @@ const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record,
{...DISABLE_AUTO_COMPLETE} {...DISABLE_AUTO_COMPLETE}
/> />
</Form.Item> </Form.Item>
<Form.Item style={{ display: 'none' }} name="id" initialValue={v4().replace(/-/g, '')}><input /></Form.Item> <Form.Item style={{ display: 'none' }} name="id" initialValue={v4().replace(/-/g, '')}>
<input />
</Form.Item>
</Form> </Form>
<div className="tip"> <div className="tip">
<p>The file supports only {csvTip} and {jsonTip} formats.</p> <p>
The file supports only {csvTip} and {jsonTip} formats.
</p>
</div> </div>
</> </>
) );
} };
export default forwardRef(SyncForm); export default forwardRef(SyncForm);

View File

@@ -26,7 +26,7 @@ export const syncColumns = () => [
dataIndex: 'path', dataIndex: 'path',
key: 'path', key: 'path',
width: 180, width: 180,
render: (_: string, row: any) => <RenderPath row={row} /> render: (_: string, row: any) => <RenderPath row={row} />,
}, },
{ {
title: 'Last updated', title: 'Last updated',
@@ -56,7 +56,11 @@ export const syncColumns = () => [
> >
<a>Sync</a> <a>Sync</a>
</Popconfirm> </Popconfirm>
{row.last_updated && <Link to={`${row.id}`} state={row}>View</Link>} {row.last_updated && (
<Link to={`${row.id}`} state={row}>
View
</Link>
)}
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a> <a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
<Popconfirm <Popconfirm
title="Are you sure to delete this path?" title="Are you sure to delete this path?"
@@ -67,23 +71,23 @@ export const syncColumns = () => [
<a>Delete</a> <a>Delete</a>
</Popconfirm> </Popconfirm>
</Space> </Space>
) );
} },
} },
]; ];
const RenderPath = ({ row }: any) => { const RenderPath = ({ row }: any) => {
const [filePath, setFilePath] = useState(''); const [filePath, setFilePath] = useState('');
useInit(async () => { useInit(async () => {
setFilePath(await getPath(row)); setFilePath(await getPath(row));
}) });
return <a onClick={() => shell.open(filePath)}>{filePath}</a> return <a onClick={() => shell.open(filePath)}>{filePath}</a>;
}; };
export const getPath = async (row: any) => { export const getPath = async (row: any) => {
if (!/^http/.test(row.protocol)) { if (!/^http/.test(row.protocol)) {
return await path.join(await chatRoot(), row.path) + `.${row.ext}`; return (await path.join(await chatRoot(), row.path)) + `.${row.ext}`;
} else { } else {
return `${row.protocol}://${row.path}.${row.ext}`; return `${row.protocol}://${row.path}.${row.ext}`;
} }
} };

View File

@@ -3,14 +3,20 @@ import { Table, Modal, Button, message } from 'antd';
import { invoke, path, fs } from '@tauri-apps/api'; import { invoke, path, fs } from '@tauri-apps/api';
import useData from '@/hooks/useData'; import useData from '@/hooks/useData';
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
import useColumns from '@/hooks/useColumns'; import useColumns from '@/hooks/useColumns';
import { TABLE_PAGINATION } from '@/hooks/useTable'; import { TABLE_PAGINATION } from '@/hooks/useTable';
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
import { CHAT_MODEL_JSON, chatRoot, readJSON, genCmd } from '@/utils'; import { CHAT_MODEL_JSON, chatRoot, readJSON, genCmd } from '@/utils';
import { syncColumns, getPath } from './config'; import { syncColumns, getPath } from './config';
import SyncForm from './Form'; import SyncForm from './Form';
const fmtData = (data: Record<string, any>[] = []) => (Array.isArray(data) ? data : []).map((i) => ({ ...i, cmd: i.cmd ? i.cmd : genCmd(i.act), tags: ['user-sync'], enable: true })); const fmtData = (data: Record<string, any>[] = []) =>
(Array.isArray(data) ? data : []).map((i) => ({
...i,
cmd: i.cmd ? i.cmd : genCmd(i.act),
tags: ['user-sync'],
enable: true,
}));
export default function SyncCustom() { export default function SyncCustom() {
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
@@ -37,7 +43,10 @@ export default function SyncCustom() {
handleSync(filename).then((isOk: boolean) => { handleSync(filename).then((isOk: boolean) => {
opInfo.resetRecord(); opInfo.resetRecord();
if (!isOk) return; if (!isOk) return;
const data = opReplace(opInfo?.opRecord?.[opSafeKey], { ...opInfo?.opRecord, last_updated: Date.now() }); const data = opReplace(opInfo?.opRecord?.[opSafeKey], {
...opInfo?.opRecord,
last_updated: Date.now(),
});
modelSet(data); modelSet(data);
opInfo.resetRecord(); opInfo.resetRecord();
}); });
@@ -48,7 +57,11 @@ export default function SyncCustom() {
if (['delete'].includes(opInfo.opType)) { if (['delete'].includes(opInfo.opType)) {
(async () => { (async () => {
try { try {
const file = await path.join(await chatRoot(), 'cache_model', `${opInfo?.opRecord?.id}.json`); const file = await path.join(
await chatRoot(),
'cache_model',
`${opInfo?.opRecord?.id}.json`,
);
await fs.removeFile(file); await fs.removeFile(file);
} catch (e) {} } catch (e) {}
const data = opRemove(opInfo?.opRecord?.[opSafeKey]); const data = opRemove(opInfo?.opRecord?.[opSafeKey]);
@@ -94,22 +107,25 @@ export default function SyncCustom() {
}; };
const handleOk = () => { const handleOk = () => {
formRef.current?.form?.validateFields() formRef.current?.form?.validateFields().then((vals: Record<string, any>) => {
.then((vals: Record<string, any>) => { if (opInfo.opType === 'new') {
let data = []; const data = opAdd(vals);
switch (opInfo.opType) {
case 'new': data = opAdd(vals); break;
case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break;
default: break;
}
modelSet(data); modelSet(data);
message.success('Data added successfully');
}
if (opInfo.opType === 'edit') {
const data = opReplace(opInfo?.opRecord?.[opSafeKey], vals);
modelSet(data);
message.success('Data updated successfully');
}
hide(); hide();
}) });
}; };
return ( return (
<div> <div>
<Button <Button
style={{ marginBottom: 10 }}
className="chat-add-btn" className="chat-add-btn"
type="primary" type="primary"
onClick={opInfo.opNew} onClick={opInfo.opNew}
@@ -135,5 +151,5 @@ export default function SyncCustom() {
<SyncForm ref={formRef} record={opInfo?.opRecord} type={opInfo.opType} /> <SyncForm ref={formRef} record={opInfo?.opRecord} type={opInfo.opType} />
</Modal> </Modal>
</div> </div>
) );
} }

View File

@@ -41,8 +41,6 @@ export const syncColumns = () => [
dataIndex: 'prompt', dataIndex: 'prompt',
key: 'prompt', key: 'prompt',
// width: 300, // width: 300,
render: (v: string) => ( render: (v: string) => <span className="chat-prompts-val">{v}</span>,
<span className="chat-prompts-val">{v}</span>
),
}, },
]; ];

View File

@@ -1,4 +1,5 @@
.chat-table-tip, .chat-table-btns { .chat-table-tip,
.chat-table-btns {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }

View File

@@ -1,10 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Table, Button, Popconfirm } from 'antd'; import { Table, Button, Popconfirm } from 'antd';
import { invoke, path, shell } from '@tauri-apps/api'; import { invoke, path } from '@tauri-apps/api';
import useInit from '@/hooks/useInit'; import useInit from '@/hooks/useInit';
import useData from '@/hooks/useData'; import useData from '@/hooks/useData';
import useColumns from '@/hooks/useColumns'; import useColumns from '@/hooks/useColumns';
import FilePath from '@/components/FilePath';
import useChatModel, { useCacheModel } from '@/hooks/useChatModel'; import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { fmtDate, chatRoot } from '@/utils'; import { fmtDate, chatRoot } from '@/utils';
@@ -51,7 +52,7 @@ export default function SyncPrompts() {
}, [opInfo.opTime]); }, [opInfo.opTime]);
const handleEnable = (isEnable: boolean) => { const handleEnable = (isEnable: boolean) => {
const data = opReplaceItems(selectedRowIDs, { enable: isEnable }) const data = opReplaceItems(selectedRowIDs, { enable: isEnable });
modelCacheSet(data); modelCacheSet(data);
}; };
@@ -71,7 +72,9 @@ export default function SyncPrompts() {
<div> <div>
{selectedItems.length > 0 && ( {selectedItems.length > 0 && (
<> <>
<Button type="primary" onClick={() => handleEnable(true)}>Enable</Button> <Button type="primary" onClick={() => handleEnable(true)}>
Enable
</Button>
<Button onClick={() => handleEnable(false)}>Disable</Button> <Button onClick={() => handleEnable(false)}>Disable</Button>
<span className="num">Selected {selectedItems.length} items</span> <span className="num">Selected {selectedItems.length} items</span>
</> </>
@@ -80,10 +83,14 @@ export default function SyncPrompts() {
</div> </div>
<div className="chat-table-tip"> <div className="chat-table-tip">
<div className="chat-sync-path"> <div className="chat-sync-path">
<div>PATH: <a onClick={() => shell.open(promptsURL)} target="_blank" title={promptsURL}>f/awesome-chatgpt-prompts/prompts.csv</a></div> <FilePath url={promptsURL} content="f/awesome-chatgpt-prompts/prompts.csv" />
<div>CACHE: <a onClick={() => shell.open(jsonPath)} target="_blank" title={jsonPath}>{jsonPath}</a></div> <FilePath label="CACHE" paths="cache_model/chatgpt_prompts.json" />
</div> </div>
{lastUpdated && <span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>Last updated on {fmtDate(lastUpdated)}</span>} {lastUpdated && (
<span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>
Last updated on {fmtDate(lastUpdated)}
</span>
)}
</div> </div>
<Table <Table
key={lastUpdated} key={lastUpdated}
@@ -93,8 +100,10 @@ export default function SyncPrompts() {
dataSource={opData} dataSource={opData}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={TABLE_PAGINATION} pagination={TABLE_PAGINATION}
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}} expandable={{
expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>,
}}
/> />
</div> </div>
) );
} }

View File

@@ -25,7 +25,11 @@ export const syncColumns = () => [
key: 'tags', key: 'tags',
// width: 150, // width: 150,
render: (v: string[]) => ( render: (v: string[]) => (
<span className="chat-prompts-tags">{v?.map(i => <Tag key={i}>{i}</Tag>)}</span> <span className="chat-prompts-tags">
{v?.map((i) => (
<Tag key={i}>{i}</Tag>
))}
</span>
), ),
}, },
{ {
@@ -43,8 +47,6 @@ export const syncColumns = () => [
dataIndex: 'prompt', dataIndex: 'prompt',
key: 'prompt', key: 'prompt',
// width: 300, // width: 300,
render: (v: string) => ( render: (v: string) => <span className="chat-prompts-val">{v}</span>,
<span className="chat-prompts-val">{v}</span>
),
}, },
]; ];

View File

@@ -2,14 +2,15 @@ import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { ArrowLeftOutlined } from '@ant-design/icons'; import { ArrowLeftOutlined } from '@ant-design/icons';
import { Table, Button } from 'antd'; import { Table, Button } from 'antd';
import { shell, path } from '@tauri-apps/api'; import { path } from '@tauri-apps/api';
import useColumns from '@/hooks/useColumns';
import useData from '@/hooks/useData'; import useData from '@/hooks/useData';
import useColumns from '@/hooks/useColumns';
import FilePath from '@/components/FilePath';
import { useCacheModel } from '@/hooks/useChatModel'; import { useCacheModel } from '@/hooks/useChatModel';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { fmtDate, chatRoot } from '@/utils';
import { getPath } from '@/view/model/SyncCustom/config'; import { getPath } from '@/view/model/SyncCustom/config';
import { fmtDate, chatRoot } from '@/utils';
import { syncColumns } from './config'; import { syncColumns } from './config';
import useInit from '@/hooks/useInit'; import useInit from '@/hooks/useInit';
@@ -29,7 +30,7 @@ export default function SyncRecord() {
useInit(async () => { useInit(async () => {
setFilePath(await getPath(state)); setFilePath(await getPath(state));
setJsonPath(await path.join(await chatRoot(), 'cache_model', `${state?.id}.json`)); setJsonPath(await path.join(await chatRoot(), 'cache_model', `${state?.id}.json`));
}) });
useEffect(() => { useEffect(() => {
if (modelCacheJson.length <= 0) return; if (modelCacheJson.length <= 0) return;
@@ -44,7 +45,7 @@ export default function SyncRecord() {
}, [opInfo.opTime]); }, [opInfo.opTime]);
const handleEnable = (isEnable: boolean) => { const handleEnable = (isEnable: boolean) => {
const data = opReplaceItems(selectedRowIDs, { enable: isEnable }) const data = opReplaceItems(selectedRowIDs, { enable: isEnable });
modelCacheSet(data); modelCacheSet(data);
}; };
@@ -57,7 +58,9 @@ export default function SyncRecord() {
<div> <div>
{selectedItems.length > 0 && ( {selectedItems.length > 0 && (
<> <>
<Button type="primary" onClick={() => handleEnable(true)}>Enable</Button> <Button type="primary" onClick={() => handleEnable(true)}>
Enable
</Button>
<Button onClick={() => handleEnable(false)}>Disable</Button> <Button onClick={() => handleEnable(false)}>Disable</Button>
<span className="num">Selected {selectedItems.length} items</span> <span className="num">Selected {selectedItems.length} items</span>
</> </>
@@ -66,10 +69,14 @@ export default function SyncRecord() {
</div> </div>
<div className="chat-table-tip"> <div className="chat-table-tip">
<div className="chat-sync-path"> <div className="chat-sync-path">
<div>PATH: <a onClick={() => shell.open(filePath)} target="_blank" title={filePath}>{filePath}</a></div> <FilePath url={filePath} />
<div>CACHE: <a onClick={() => shell.open(jsonPath)} target="_blank" title={jsonPath}>{jsonPath}</a></div> <FilePath label="CACHE" paths={`cache_model/${state?.id}.json`} />
</div> </div>
{state?.last_updated && <span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>Last updated on {fmtDate(state?.last_updated)}</span>} {state?.last_updated && (
<span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>
Last updated on {fmtDate(state?.last_updated)}
</span>
)}
</div> </div>
<Table <Table
key="prompt" key="prompt"
@@ -79,8 +86,10 @@ export default function SyncRecord() {
dataSource={opData} dataSource={opData}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={TABLE_PAGINATION} pagination={TABLE_PAGINATION}
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}} expandable={{
expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>,
}}
/> />
</div> </div>
) );
} }

View File

@@ -16,7 +16,10 @@ const initFormValue = {
prompt: '', prompt: '',
}; };
const UserCustomForm: ForwardRefRenderFunction<FormProps, UserCustomFormProps> = ({ record }, ref) => { const UserCustomForm: ForwardRefRenderFunction<FormProps, UserCustomFormProps> = (
{ record },
ref,
) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
useImperativeHandle(ref, () => ({ form })); useImperativeHandle(ref, () => ({ form }));
@@ -27,24 +30,20 @@ const UserCustomForm: ForwardRefRenderFunction<FormProps, UserCustomFormProps> =
}, [record]); }, [record]);
return ( return (
<Form <Form form={form} labelCol={{ span: 4 }} initialValues={initFormValue}>
form={form}
labelCol={{ span: 4 }}
initialValues={initFormValue}
>
<Form.Item <Form.Item
label="/{cmd}" label="/{cmd}"
name="cmd" name="cmd"
rules={[{ required: true, message: 'Please input {cmd}!' }]} rules={[{ required: true, message: 'Please enter the {cmd}!' }]}
> >
<Input placeholder="Please input {cmd}" {...DISABLE_AUTO_COMPLETE} /> <Input placeholder="Please enter the {cmd}" {...DISABLE_AUTO_COMPLETE} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="Act" label="Act"
name="act" name="act"
rules={[{ required: true, message: 'Please input act!' }]} rules={[{ required: true, message: 'Please enter the Act!' }]}
> >
<Input placeholder="Please input act" {...DISABLE_AUTO_COMPLETE} /> <Input placeholder="Please enter the Act" {...DISABLE_AUTO_COMPLETE} />
</Form.Item> </Form.Item>
<Form.Item label="Tags" name="tags"> <Form.Item label="Tags" name="tags">
<Tags value={record?.tags} /> <Tags value={record?.tags} />
@@ -55,12 +54,12 @@ const UserCustomForm: ForwardRefRenderFunction<FormProps, UserCustomFormProps> =
<Form.Item <Form.Item
label="Prompt" label="Prompt"
name="prompt" name="prompt"
rules={[{ required: true, message: 'Please input prompt!' }]} rules={[{ required: true, message: 'Please enter a prompt!' }]}
> >
<Input.TextArea rows={4} placeholder="Please input prompt" {...DISABLE_AUTO_COMPLETE} /> <Input.TextArea rows={4} placeholder="Please enter a prompt" {...DISABLE_AUTO_COMPLETE} />
</Form.Item> </Form.Item>
</Form> </Form>
) );
} };
export default forwardRef(UserCustomForm); export default forwardRef(UserCustomForm);

View File

@@ -7,7 +7,7 @@ export const modelColumns = () => [
fixed: 'left', fixed: 'left',
width: 120, width: 120,
key: 'cmd', key: 'cmd',
render: (v: string) => <Tag color="#2a2a2a">/{v}</Tag> render: (v: string) => <Tag color="#2a2a2a">/{v}</Tag>,
}, },
{ {
title: 'Act', title: 'Act',
@@ -21,7 +21,11 @@ export const modelColumns = () => [
key: 'tags', key: 'tags',
width: 150, width: 150,
render: (v: string[]) => ( render: (v: string[]) => (
<span className="chat-prompts-tags">{v?.map(i => <Tag key={i}>{i}</Tag>)}</span> <span className="chat-prompts-tags">
{v?.map((i) => (
<Tag key={i}>{i}</Tag>
))}
</span>
), ),
}, },
{ {
@@ -39,9 +43,7 @@ export const modelColumns = () => [
dataIndex: 'prompt', dataIndex: 'prompt',
key: 'prompt', key: 'prompt',
width: 300, width: 300,
render: (v: string) => ( render: (v: string) => <span className="chat-prompts-val">{v}</span>,
<span className="chat-prompts-val">{v}</span>
),
}, },
{ {
title: 'Action', title: 'Action',
@@ -61,5 +63,5 @@ export const modelColumns = () => [
</Popconfirm> </Popconfirm>
</Space> </Space>
), ),
} },
]; ];

View File

@@ -1,17 +1,18 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Table, Button, Modal, message } from 'antd'; import { Table, Button, Modal, message } from 'antd';
import { shell, path } from '@tauri-apps/api'; import { path } from '@tauri-apps/api';
import useInit from '@/hooks/useInit'; import useInit from '@/hooks/useInit';
import useData from '@/hooks/useData'; import useData from '@/hooks/useData';
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
import useColumns from '@/hooks/useColumns'; import useColumns from '@/hooks/useColumns';
import FilePath from '@/components/FilePath';
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { chatRoot, fmtDate } from '@/utils'; import { chatRoot, fmtDate } from '@/utils';
import { modelColumns } from './config'; import { modelColumns } from './config';
import UserCustomForm from './Form'; import UserCustomForm from './Form';
export default function LanguageModel() { export default function UserCustom() {
const { rowSelection, selectedRowIDs } = useTableRowSelection(); const { rowSelection, selectedRowIDs } = useTableRowSelection();
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const [jsonPath, setJsonPath] = useState(''); const [jsonPath, setJsonPath] = useState('');
@@ -44,14 +45,6 @@ export default function LanguageModel() {
} }
}, [opInfo.opType, formRef]); }, [opInfo.opType, formRef]);
useEffect(() => {
if (opInfo.opType === 'enable') {
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
modelCacheSet(data);
}
}, [opInfo.opTime])
useEffect(() => { useEffect(() => {
if (opInfo.opType === 'enable') { if (opInfo.opType === 'enable') {
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord); const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
@@ -60,7 +53,7 @@ export default function LanguageModel() {
}, [opInfo.opTime]); }, [opInfo.opTime]);
const handleEnable = (isEnable: boolean) => { const handleEnable = (isEnable: boolean) => {
const data = opReplaceItems(selectedRowIDs, { enable: isEnable }) const data = opReplaceItems(selectedRowIDs, { enable: isEnable });
modelCacheSet(data); modelCacheSet(data);
}; };
@@ -70,17 +63,26 @@ export default function LanguageModel() {
}; };
const handleOk = () => { const handleOk = () => {
formRef.current?.form?.validateFields() formRef.current?.form?.validateFields().then(async (vals: Record<string, any>) => {
.then(async (vals: Record<string, any>) => { if (
if (modelCacheJson.map((i: any) => i.cmd).includes(vals.cmd) && opInfo?.opRecord?.cmd !== vals.cmd) { modelCacheJson.map((i: any) => i.cmd).includes(vals.cmd) &&
message.warning(`"cmd: /${vals.cmd}" already exists, please change the "${vals.cmd}" name and resubmit.`); opInfo?.opRecord?.cmd !== vals.cmd
) {
message.warning(
`"cmd: /${vals.cmd}" already exists, please change the "${vals.cmd}" name and resubmit.`,
);
return; return;
} }
let data = []; let data = [];
switch (opInfo.opType) { switch (opInfo.opType) {
case 'new': data = opAdd(vals); break; case 'new':
case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break; data = opAdd(vals);
default: break; break;
case 'edit':
data = opReplace(opInfo?.opRecord?.[opSafeKey], vals);
break;
default:
break;
} }
await modelCacheSet(data); await modelCacheSet(data);
opInit(data); opInit(data);
@@ -89,31 +91,36 @@ export default function LanguageModel() {
last_updated: Date.now(), last_updated: Date.now(),
}); });
hide(); hide();
}) });
}; };
const modalTitle = `${({ new: 'Create', edit: 'Edit' })[opInfo.opType]} Model`; const modalTitle = `${{ new: 'Create', edit: 'Edit' }[opInfo.opType]} Model`;
return ( return (
<div> <div>
<div className="chat-table-btns"> <div className="chat-table-btns">
<Button className="chat-add-btn" type="primary" onClick={opInfo.opNew}>Add Model</Button> <Button className="chat-add-btn" type="primary" onClick={opInfo.opNew}>
Add Model
</Button>
<div> <div>
{selectedItems.length > 0 && ( {selectedItems.length > 0 && (
<> <>
<Button type="primary" onClick={() => handleEnable(true)}>Enable</Button> <Button type="primary" onClick={() => handleEnable(true)}>
Enable
</Button>
<Button onClick={() => handleEnable(false)}>Disable</Button> <Button onClick={() => handleEnable(false)}>Disable</Button>
<span className="num">Selected {selectedItems.length} items</span> <span className="num">Selected {selectedItems.length} items</span>
</> </>
)} )}
</div> </div>
</div> </div>
{/* <div className="chat-model-path">PATH: <span onClick={handleOpenFile}>{modelPath}</span></div> */}
<div className="chat-table-tip"> <div className="chat-table-tip">
<div className="chat-sync-path"> <FilePath label="CACHE" paths="cache_model/user_custom.json" />
<div>CACHE: <a onClick={() => shell.open(jsonPath)} title={jsonPath}>{jsonPath}</a></div> {lastUpdated && (
</div> <span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>
{lastUpdated && <span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>Last updated on {fmtDate(lastUpdated)}</span>} Last updated on {fmtDate(lastUpdated)}
</span>
)}
</div> </div>
<Table <Table
key={lastUpdated} key={lastUpdated}
@@ -123,7 +130,9 @@ export default function LanguageModel() {
dataSource={opData} dataSource={opData}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={TABLE_PAGINATION} pagination={TABLE_PAGINATION}
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}} expandable={{
expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>,
}}
/> />
<Modal <Modal
open={isVisible} open={isVisible}
@@ -136,5 +145,5 @@ export default function LanguageModel() {
<UserCustomForm record={opInfo?.opRecord} ref={formRef} /> <UserCustomForm record={opInfo?.opRecord} ref={formRef} />
</Modal> </Modal>
</div> </div>
) );
} }

View File

@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Space, Popconfirm } from 'antd'; import { Space, Popconfirm } from 'antd';
import { path, shell } from '@tauri-apps/api'; import { path, shell } from '@tauri-apps/api';
@@ -40,7 +41,9 @@ export const notesColumns = () => [
return ( return (
<Space> <Space>
<a onClick={() => actions.setRecord(row, 'preview')}>Preview</a> <a onClick={() => actions.setRecord(row, 'preview')}>Preview</a>
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a> <Link to={`/md/${row.id}`} state={row}>
Edit
</Link>
<Popconfirm <Popconfirm
title="Are you sure to delete this file?" title="Are you sure to delete this file?"
onConfirm={() => actions.setRecord(row, 'delete')} onConfirm={() => actions.setRecord(row, 'delete')}
@@ -50,20 +53,19 @@ export const notesColumns = () => [
<a>Delete</a> <a>Delete</a>
</Popconfirm> </Popconfirm>
</Space> </Space>
) );
} },
} },
]; ];
const RenderPath = ({ row }: any) => { const RenderPath = ({ row }: any) => {
const [filePath, setFilePath] = useState(''); const [filePath, setFilePath] = useState('');
useInit(async () => { useInit(async () => {
setFilePath(await getPath(row)); setFilePath(await getPath(row));
}) });
return <a onClick={() => shell.open(filePath)}>{filePath}</a>; return <a onClick={() => shell.open(filePath)}>{filePath}</a>;
}; };
export const getPath = async (row: any) => { export const getPath = async (row: any) => {
const isImg = ['png'].includes(row?.ext); return (await path.join(await chatRoot(), 'notes', row.id)) + `.${row.ext}`;
return await path.join(await chatRoot(), 'notes', row.id) + `.${row.ext}`; };
}

View File

@@ -1,20 +1,17 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Table, Modal, Popconfirm, Button, message } from 'antd'; import { Table, Modal, Popconfirm, Button, message } from 'antd';
import { invoke, path, shell, fs } from '@tauri-apps/api'; import { invoke, path, fs } from '@tauri-apps/api';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import useInit from '@/hooks/useInit';
import useJson from '@/hooks/useJson'; import useJson from '@/hooks/useJson';
import useData from '@/hooks/useData'; import useData from '@/hooks/useData';
import useColumns from '@/hooks/useColumns'; import useColumns from '@/hooks/useColumns';
import Markdown from '@/components/Markdown';
import FilePath from '@/components/FilePath';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable'; import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { chatRoot, CHAT_NOTES_JSON } from '@/utils'; import { chatRoot, CHAT_NOTES_JSON } from '@/utils';
import { notesColumns } from './config'; import { notesColumns } from './config';
export default function Notes() { export default function Notes() {
const [notesPath, setNotesPath] = useState('');
const [source, setSource] = useState(''); const [source, setSource] = useState('');
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const { opData, opInit, opReplace, opSafeKey } = useData([]); const { opData, opInit, opReplace, opSafeKey } = useData([]);
@@ -23,11 +20,6 @@ export default function Notes() {
const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_NOTES_JSON); const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_NOTES_JSON);
const selectedItems = rowSelection.selectedRowKeys || []; const selectedItems = rowSelection.selectedRowKeys || [];
useInit(async () => {
const file = await path.join(await chatRoot(), CHAT_NOTES_JSON);
setNotesPath(file);
});
useEffect(() => { useEffect(() => {
if (!json || json.length <= 0) return; if (!json || json.length <= 0) return;
opInit(json); opInit(json);
@@ -44,9 +36,6 @@ export default function Notes() {
setVisible(true); setVisible(true);
return; return;
} }
if (opInfo.opType === 'edit') {
alert('TODO');
}
if (opInfo.opType === 'delete') { if (opInfo.opType === 'delete') {
await fs.removeFile(file); await fs.removeFile(file);
await handleRefresh(); await handleRefresh();
@@ -57,8 +46,8 @@ export default function Notes() {
message.success('Name has been changed!'); message.success('Name has been changed!');
} }
opInfo.resetRecord(); opInfo.resetRecord();
})() })();
}, [opInfo.opType]) }, [opInfo.opType]);
const handleDelete = async () => { const handleDelete = async () => {
if (opData?.length === selectedRows.length) { if (opData?.length === selectedRows.length) {
@@ -73,7 +62,7 @@ export default function Notes() {
const file = await path.join(await chatRoot(), 'notes', `${i?.id}.${i?.ext}`); const file = await path.join(await chatRoot(), 'notes', `${i?.id}.${i?.ext}`);
await fs.removeFile(file); await fs.removeFile(file);
return file; return file;
}) });
Promise.all(rows).then(async () => { Promise.all(rows).then(async () => {
await handleRefresh(); await handleRefresh();
message.success('All files selected are cleared!'); message.success('All files selected are cleared!');
@@ -106,18 +95,14 @@ export default function Notes() {
okText="Yes" okText="Yes"
cancelText="No" cancelText="No"
> >
<Button>Batch delete</Button> <Button>Delete</Button>
</Popconfirm> </Popconfirm>
<span className="num">Selected {selectedItems.length} items</span> <span className="num">Selected {selectedItems.length} items</span>
</> </>
)} )}
</div> </div>
</div> </div>
<div className="chat-table-tip"> <FilePath paths={CHAT_NOTES_JSON} />
<div className="chat-file-path">
<div>PATH: <a onClick={() => shell.open(notesPath)} title={notesPath}>{notesPath}</a></div>
</div>
</div>
<Table <Table
rowKey="id" rowKey="id"
columns={columns} columns={columns}
@@ -132,30 +117,10 @@ export default function Notes() {
onCancel={handleCancel} onCancel={handleCancel}
footer={false} footer={false}
destroyOnClose destroyOnClose
width={600}
> >
<ReactMarkdown <Markdown children={source} />
children={source}
linkTarget="_blank"
components={{
code({node, inline, className, children, ...props}) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, '')}
style={a11yDark as any}
language={match[1]}
PreTag="div"
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
/>
</Modal> </Modal>
</div> </div>
) );
} }

95
src/view/settings/General.tsx vendored Normal file
View File

@@ -0,0 +1,95 @@
import { useState } from 'react';
import { Form, Radio, Switch, Input, Tooltip } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { platform } from '@tauri-apps/api/os';
import useInit from '@/hooks/useInit';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
export default function General() {
const [platformInfo, setPlatform] = useState('');
useInit(async () => {
setPlatform(await platform());
});
return (
<>
<Form.Item label="Stay On Top" name="stay_on_top" valuePropName="checked">
<Switch />
</Form.Item>
{platformInfo === 'darwin' && (
<Form.Item label="Titlebar" name="titlebar" valuePropName="checked">
<Switch />
</Form.Item>
)}
{platformInfo === 'darwin' && (
<Form.Item label="Hide Dock Icon" name="hide_dock_icon" valuePropName="checked">
<Switch />
</Form.Item>
)}
<Form.Item label="Theme" name="theme">
<Radio.Group>
<Radio value="light">Light</Radio>
<Radio value="dark">Dark</Radio>
{['darwin', 'windows'].includes(platformInfo) && <Radio value="System">System</Radio>}
</Radio.Group>
</Form.Item>
<Form.Item label={<AutoUpdateLabel />} name="auto_update">
<Radio.Group>
<Radio value="prompt">Prompt</Radio>
<Radio value="silent">Silent</Radio>
{/*<Radio value="disable">Disable</Radio>*/}
</Radio.Group>
</Form.Item>
<Form.Item label={<GlobalShortcutLabel />} name="global_shortcut">
<Input placeholder="CmdOrCtrl+Shift+O" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
</>
);
}
const AutoUpdateLabel = () => {
return (
<span>
Auto Update{' '}
<Tooltip
title={
<div>
<div>Auto Update Policy</div>
<div>
<strong>Prompt</strong>: prompt to install
</div>
<div>
<strong>Silent</strong>: install silently
</div>
{/* <div><strong>Disable</strong>: disable auto update</div> */}
</div>
}
>
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
</Tooltip>
</span>
);
};
const GlobalShortcutLabel = () => {
return (
<div>
Global Shortcut{' '}
<Tooltip
title={
<div>
<div>Shortcut definition, modifiers and key separated by "+" e.g. CmdOrControl+Q</div>
<div style={{ margin: '10px 0' }}>If empty, the shortcut is disabled.</div>
<a href="https://tauri.app/v1/api/js/globalshortcut" target="_blank">
https://tauri.app/v1/api/js/globalshortcut
</a>
</div>
}
>
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
</Tooltip>
</div>
);
};

49
src/view/settings/MainWindow.tsx vendored Normal file
View File

@@ -0,0 +1,49 @@
import { Form, Switch, Input, Tooltip } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import SwitchOrigin from '@/components/SwitchOrigin';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
const PopupSearchLabel = () => {
return (
<span>
Pop-up Search{' '}
<Tooltip
title={
<div>
<div style={{ marginBottom: 10 }}>
Generate images according to the content: Select the ChatGPT content with the mouse,
no more than 400 characters. the <b>DALL·E 2</b> button appears, and click to jump
(Note: because the search content filled by the script cannot trigger the event
directly, you need to enter a space in the input box to make the button clickable).
</div>
<div>
The application is built using Tauri, and due to its security restrictions, some of
the action buttons will not work, so we recommend going to your browser.
</div>
</div>
}
>
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
</Tooltip>
</span>
);
};
export default function MainWindow() {
return (
<>
<Form.Item label={<PopupSearchLabel />} name="popup_search" valuePropName="checked">
<Switch />
</Form.Item>
<SwitchOrigin name="main" />
<Form.Item label="User Agent (Main)" name="ua_window">
<Input.TextArea
autoSize={{ minRows: 4, maxRows: 4 }}
{...DISABLE_AUTO_COMPLETE}
placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
/>
</Form.Item>
</>
);
}

36
src/view/settings/TrayWindow.tsx vendored Normal file
View File

@@ -0,0 +1,36 @@
import { Form, Switch, Input, Tooltip } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
import SwitchOrigin from '@/components/SwitchOrigin';
const UALabel = () => {
return (
<span>
User Agent (SystemTray){' '}
<Tooltip
title={<div>For a better experience, we recommend using the Mobile User-Agent.</div>}
>
<QuestionCircleOutlined style={{ color: '#1677ff' }} />
</Tooltip>
</span>
);
};
export default function TrayWindow() {
return (
<>
<Form.Item label="Enable SystemTray" name="tray" valuePropName="checked">
<Switch />
</Form.Item>
<SwitchOrigin name="tray" />
<Form.Item label={<UALabel />} name="ua_tray">
<Input.TextArea
autoSize={{ minRows: 4, maxRows: 4 }}
{...DISABLE_AUTO_COMPLETE}
placeholder="Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1"
/>
</Form.Item>
</>
);
}

117
src/view/settings/index.tsx vendored Normal file
View File

@@ -0,0 +1,117 @@
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Form, Tabs, Space, Button, Popconfirm, message } from 'antd';
import { invoke, dialog, process, path, shell } from '@tauri-apps/api';
import { clone, omit, isEqual } from 'lodash';
import useInit from '@/hooks/useInit';
import FilePath from '@/components/FilePath';
import { chatRoot, APP_CONF_JSON } from '@/utils';
import General from './General';
import MainWindow from './MainWindow';
import TrayWindow from './TrayWindow';
export default function Settings() {
const [params] = useSearchParams();
const [activeKey, setActiveKey] = useState('general');
const [form] = Form.useForm();
const [chatConf, setChatConf] = useState<any>(null);
const [filePath, setPath] = useState('');
const key = params.get('type');
useEffect(() => {
setActiveKey(key ? key : 'general');
}, [key]);
useInit(async () => {
setChatConf(await invoke('get_app_conf'));
setPath(await path.join(await chatRoot(), APP_CONF_JSON));
});
useEffect(() => {
form.setFieldsValue(clone(chatConf));
}, [chatConf]);
const onCancel = () => {
form.setFieldsValue(chatConf);
};
const onReset = async () => {
const chatData = await invoke('reset_app_conf');
setChatConf(chatData);
const isOk = await dialog.ask(`Configuration reset successfully, whether to restart?`, {
title: 'ChatGPT Preferences',
});
if (isOk) {
process.relaunch();
return;
}
message.success('Configuration reset successfully');
};
const onFinish = async (values: any) => {
if (!isEqual(omit(chatConf, ['default_origin']), values)) {
await invoke('form_confirm', { data: values, label: 'main' });
const isOk = await dialog.ask(`Configuration saved successfully, whether to restart?`, {
title: 'ChatGPT Preferences',
});
if (isOk) {
process.relaunch();
return;
}
message.success('Configuration saved successfully');
}
};
const handleTab = (v: string) => {
setActiveKey(v);
};
return (
<div>
<FilePath paths={APP_CONF_JSON} />
<Form
form={form}
style={{ maxWidth: 500 }}
onFinish={onFinish}
labelCol={{ span: 10 }}
wrapperCol={{ span: 13, offset: 1 }}
>
<Tabs
activeKey={activeKey}
onChange={handleTab}
items={[
{ label: 'General', key: 'general', children: <General /> },
{ label: 'Main Window', key: 'main_window', children: <MainWindow /> },
{ label: 'SystemTray Window', key: 'tray_window', children: <TrayWindow /> },
]}
/>
<Form.Item>
<Space size={20}>
<Button onClick={onCancel}>Cancel</Button>
<Button type="primary" htmlType="submit">
Submit
</Button>
<Popconfirm
title={
<div style={{ width: 360 }}>
Are you sure you want to reset the configuration file
<a onClick={() => shell.open(filePath)} style={{ margin: '0 5px' }}>
{filePath}
</a>
to the default?
</div>
}
onConfirm={onReset}
okText="Yes"
cancelText="No"
>
<Button type="dashed">Reset to defaults</Button>
</Popconfirm>
</Space>
</Form.Item>
</Form>
</div>
);
}

View File

@@ -20,7 +20,7 @@
"@/*": ["src/*"], "@/*": ["src/*"],
"@view/*": ["src/view/*"], "@view/*": ["src/view/*"],
"@comps/*": ["src/components/*"], "@comps/*": ["src/components/*"],
"@layout/*": ["src/layout/*"], "@layout/*": ["src/layout/*"]
} }
}, },
"include": ["src"], "include": ["src"],

View File

@@ -1,6 +1,6 @@
import { defineConfig } from "vite"; import { defineConfig } from 'vite';
import react from "@vitejs/plugin-react"; import react from '@vitejs/plugin-react';
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from 'vite-tsconfig-paths';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@@ -16,12 +16,12 @@ export default defineConfig({
}, },
// to make use of `TAURI_DEBUG` and other env variables // to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ["VITE_", "TAURI_"], envPrefix: ['VITE_', 'TAURI_'],
build: { build: {
// Tauri supports es2021 // Tauri supports es2021
target: ["es2021", "chrome100", "safari13"], target: ['es2021', 'chrome100', 'safari13'],
// don't minify for debug builds // don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? "esbuild" : false, minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
// produce sourcemaps for debug builds // produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG, sourcemap: !!process.env.TAURI_DEBUG,
}, },