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,43 +1,43 @@
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: |
## Bug report ## Bug report
Please fill in the following information to help us reproduce the bug: Please fill in the following information to help us reproduce the bug:
- type: input - type: input
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
id: bug id: bug
attributes: attributes:
label: Bug description label: Bug description
description: | description: |
Please describe the bug here,if possible, please provide a minimal example to reproduce the bug.The content of `~/.chatgpt/chatgpt.log` may be helpful if you encounter a crash. Please describe the bug here,if possible, please provide a minimal example to reproduce the bug.The content of `~/.chatgpt/chatgpt.log` may be helpful if you encounter a crash.
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,37 +1,37 @@
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: |
## Build error report ## Build error report
Please fill in the following information to help us reproduce the bug: Please fill in the following information to help us reproduce the bug:
- type: textarea - type: textarea
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,19 +1,19 @@
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
attributes: attributes:
value: Please limit one request per issue. value: Please limit one request per issue.

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.

View File

@@ -91,7 +91,7 @@ jobs:
force_orphan: true force_orphan: true
# publish-winget: # publish-winget:
# # Action can only be run on windows # # Action can only be run on windows
# runs-on: windows-latest # runs-on: windows-latest
# needs: [create-release, build-tauri] # needs: [create-release, build-tauri]
# steps: # steps:

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,3 +1,3 @@
fn main() { fn main() {
tauri_build::build() tauri_build::build()
} }

View File

@@ -1,400 +1,74 @@
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();
if win.is_fullscreen().unwrap() { if win.is_fullscreen().unwrap() {
win.set_fullscreen(false).unwrap(); win.set_fullscreen(false).unwrap();
} else { } else {
win.set_fullscreen(true).unwrap(); win.set_fullscreen(true).unwrap();
} }
} }
#[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));
fs::write(&path, blob).unwrap(); utils::create_file(&path).unwrap();
utils::open_file(path); fs::write(&path, blob).unwrap();
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));
fs::write(&path, content).unwrap(); utils::create_file(&path).unwrap();
utils::open_file(path); fs::write(&path, content).unwrap();
tauri::api::dialog::message(
win.as_ref(),
"Save File",
format!("PATH: {}", path.display()),
);
} }
#[command] #[command]
pub fn open_link(app: AppHandle, url: String) { 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 { res.unwrap_or_else(|err| {
pub cmd: Option<String>, error!("chatgpt_client_http: {}", err);
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()),
};
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::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
}
#[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 None
})
} }

View File

@@ -6,8 +6,8 @@
use serde::{ser::Serializer, Serialize}; use serde::{ser::Serializer, Serialize};
use std::{ use std::{
path::PathBuf, path::PathBuf,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use tauri::command; use tauri::command;
@@ -20,101 +20,102 @@ type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
impl Serialize for Error { impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
{ {
serializer.serialize_str(self.to_string().as_ref()) serializer.serialize_str(self.to_string().as_ref())
} }
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Permissions { struct Permissions {
readonly: bool, readonly: bool,
#[cfg(unix)] #[cfg(unix)]
mode: u32, mode: u32,
} }
#[cfg(unix)] #[cfg(unix)]
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct UnixMetadata { struct UnixMetadata {
dev: u64, dev: u64,
ino: u64, ino: u64,
mode: u32, mode: u32,
nlink: u64, nlink: u64,
uid: u32, uid: u32,
gid: u32, gid: u32,
rdev: u64, rdev: u64,
blksize: u64, blksize: u64,
blocks: u64, blocks: u64,
} }
#[derive(Serialize)] #[derive(Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Metadata { pub struct Metadata {
accessed_at_ms: u64, accessed_at_ms: u64,
pub created_at_ms: u64, pub created_at_ms: u64,
modified_at_ms: u64, modified_at_ms: u64,
is_dir: bool, is_dir: bool,
is_file: bool, is_file: bool,
is_symlink: bool, is_symlink: bool,
size: u64, size: u64,
permissions: Permissions, permissions: Permissions,
#[cfg(unix)] #[cfg(unix)]
#[serde(flatten)] #[serde(flatten)]
unix: UnixMetadata, unix: UnixMetadata,
#[cfg(windows)] #[cfg(windows)]
file_attributes: u32, file_attributes: u32,
} }
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
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap(); .map(|t| {
duration_since_epoch.as_millis() as u64 let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
duration_since_epoch.as_millis() as u64
}) })
.unwrap_or_default() .unwrap_or_default()
} }
#[command] #[command]
pub async fn metadata(path: PathBuf) -> Result<Metadata> { pub async fn metadata(path: PathBuf) -> Result<Metadata> {
let metadata = std::fs::metadata(path)?; let metadata = std::fs::metadata(path)?;
let file_type = metadata.file_type(); let file_type = metadata.file_type();
let permissions = metadata.permissions(); let permissions = metadata.permissions();
Ok(Metadata { Ok(Metadata {
accessed_at_ms: system_time_to_ms(metadata.accessed()), accessed_at_ms: system_time_to_ms(metadata.accessed()),
created_at_ms: system_time_to_ms(metadata.created()), created_at_ms: system_time_to_ms(metadata.created()),
modified_at_ms: system_time_to_ms(metadata.modified()), modified_at_ms: system_time_to_ms(metadata.modified()),
is_dir: file_type.is_dir(), is_dir: file_type.is_dir(),
is_file: file_type.is_file(), is_file: file_type.is_file(),
is_symlink: file_type.is_symlink(), is_symlink: file_type.is_symlink(),
size: metadata.len(), size: metadata.len(),
permissions: Permissions { permissions: Permissions {
readonly: permissions.readonly(), readonly: permissions.readonly(),
#[cfg(unix)] #[cfg(unix)]
mode: permissions.mode(), mode: permissions.mode(),
}, },
#[cfg(unix)] #[cfg(unix)]
unix: UnixMetadata { unix: UnixMetadata {
dev: metadata.dev(), dev: metadata.dev(),
ino: metadata.ino(), ino: metadata.ino(),
mode: metadata.mode(), mode: metadata.mode(),
nlink: metadata.nlink(), nlink: metadata.nlink(),
uid: metadata.uid(), uid: metadata.uid(),
gid: metadata.gid(), gid: metadata.gid(),
rdev: metadata.rdev(), rdev: metadata.rdev(),
blksize: metadata.blksize(), blksize: metadata.blksize(),
blocks: metadata.blocks(), blocks: metadata.blocks(),
}, },
#[cfg(windows)] #[cfg(windows)]
file_attributes: metadata.file_attributes(), file_attributes: metadata.file_attributes(),
}) })
} }
// #[command] // #[command]

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,11 +1,11 @@
use crate::{ use crate::{
app::{cmd, window}, app::window,
conf::{self, ChatConfJson}, conf::{self, AppConf},
utils, utils,
}; };
use tauri::{ use tauri::{
AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent,
SystemTrayMenu, SystemTrayMenuItem, WindowMenuEvent, SystemTrayMenu, SystemTrayMenuItem, WindowMenuEvent,
}; };
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt}; use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
@@ -14,461 +14,498 @@ 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,
Menu::with_items([ Menu::with_items([
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
MenuItem::About(name.into(), AboutMetadata::default()).into(), MenuItem::About(name.into(), AboutMetadata::default()).into(),
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
CustomMenuItem::new("about".to_string(), "About ChatGPT").into(), CustomMenuItem::new("about".to_string(), "About ChatGPT").into(),
CustomMenuItem::new("check_update".to_string(), "Check for Updates").into(), CustomMenuItem::new("check_update".to_string(), "Check for Updates").into(),
MenuItem::Services.into(), MenuItem::Services.into(),
MenuItem::Hide.into(), MenuItem::Hide.into(),
MenuItem::HideOthers.into(), MenuItem::HideOthers.into(),
MenuItem::ShowAll.into(), MenuItem::ShowAll.into(),
MenuItem::Separator.into(), MenuItem::Separator.into(),
MenuItem::Quit.into(), MenuItem::Quit.into(),
]), ]),
); );
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
}; };
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 app_conf.titlebar {
let titlebar_menu = if chat_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 preferences_menu = Submenu::new( let auto_update = app_conf.get_auto_update();
"Preferences", let preferences_menu = Submenu::new(
Menu::with_items([ "Preferences",
CustomMenuItem::new("control_center".to_string(), "Control Center") Menu::with_items([
.accelerator("CmdOrCtrl+Shift+P") CustomMenuItem::new("control_center".to_string(), "Control Center")
.into(), .accelerator("CmdOrCtrl+Shift+P")
MenuItem::Separator.into(), .into(),
stay_on_top_menu.into(), MenuItem::Separator.into(),
#[cfg(target_os = "macos")] stay_on_top_menu.into(),
titlebar_menu.into(), #[cfg(target_os = "macos")]
#[cfg(target_os = "macos")] titlebar_menu.into(),
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(), #[cfg(target_os = "macos")]
system_tray_menu.into(), CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
CustomMenuItem::new("inject_script".to_string(), "Inject Script") system_tray_menu.into(),
.accelerator("CmdOrCtrl+J") CustomMenuItem::new("inject_script".to_string(), "Inject Script")
.into(), .accelerator("CmdOrCtrl+J")
MenuItem::Separator.into(), .into(),
Submenu::new( MenuItem::Separator.into(),
"Theme", Submenu::new(
Menu::new() "Theme",
.add_item(if is_dark || is_system {
theme_light
} else {
theme_light.selected()
})
.add_item(if is_dark {
theme_dark.selected()
} else {
theme_dark
})
.add_item(if is_system {
theme_system.selected()
} else {
theme_system
}),
)
.into(),
Submenu::new(
"Auto Update",
Menu::new()
.add_item(if chat_conf.auto_update == "Prompt" {
update_prompt.selected()
} else {
update_prompt
})
.add_item(if chat_conf.auto_update == "Silent" {
update_silent.selected()
} else {
update_silent
}), // .add_item(if chat_conf.auto_update == "Disable" {
// update_disable.selected()
// } else {
// update_disable
// })
)
.into(),
MenuItem::Separator.into(),
popup_search_menu.into(),
CustomMenuItem::new("sync_prompts".to_string(), "Sync Prompts").into(),
MenuItem::Separator.into(),
CustomMenuItem::new("go_conf".to_string(), "Go to Config")
.accelerator("CmdOrCtrl+Shift+G")
.into(),
CustomMenuItem::new("clear_conf".to_string(), "Clear Config")
.accelerator("CmdOrCtrl+Shift+D")
.into(),
CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
.accelerator("CmdOrCtrl+Shift+R")
.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(),
]),
);
let edit_menu = Submenu::new(
"Edit",
Menu::new() Menu::new()
.add_native_item(MenuItem::Undo) .add_item(if is_dark || is_system {
.add_native_item(MenuItem::Redo) theme_light
.add_native_item(MenuItem::Separator) } else {
.add_native_item(MenuItem::Cut) theme_light.selected()
.add_native_item(MenuItem::Copy) })
.add_native_item(MenuItem::Paste) .add_item(if is_dark {
.add_native_item(MenuItem::SelectAll), theme_dark.selected()
); } else {
theme_dark
let view_menu = Submenu::new( })
"View", .add_item(if is_system {
theme_system.selected()
} else {
theme_system
}),
)
.into(),
Submenu::new(
"Auto Update",
Menu::new() Menu::new()
.add_item( .add_item(if auto_update == "prompt" {
CustomMenuItem::new("go_back".to_string(), "Go Back").accelerator("CmdOrCtrl+Left"), update_prompt.selected()
) } else {
.add_item( update_prompt
CustomMenuItem::new("go_forward".to_string(), "Go Forward") })
.accelerator("CmdOrCtrl+Right"), .add_item(if auto_update == "silent" {
) update_silent.selected()
.add_item( } else {
CustomMenuItem::new("scroll_top".to_string(), "Scroll to Top of Screen") update_silent
.accelerator("CmdOrCtrl+Up"), }), // .add_item(if auto_update == "disable" {
) // update_disable.selected()
.add_item( // } else {
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen") // update_disable
.accelerator("CmdOrCtrl+Down"), // })
) )
.add_native_item(MenuItem::Separator) .into(),
.add_item( MenuItem::Separator.into(),
CustomMenuItem::new("reload".to_string(), "Refresh the Screen") popup_search_menu.into(),
.accelerator("CmdOrCtrl+R"), CustomMenuItem::new("sync_prompts".to_string(), "Sync Prompts").into(),
), MenuItem::Separator.into(),
); CustomMenuItem::new("go_conf".to_string(), "Go to Config")
.accelerator("CmdOrCtrl+Shift+G")
let window_menu = Submenu::new( .into(),
"Window", CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
Menu::new() .accelerator("CmdOrCtrl+Shift+R")
.add_item(CustomMenuItem::new("dalle2".to_string(), "DALL·E 2")) .into(),
.add_native_item(MenuItem::Separator) CustomMenuItem::new("clear_conf".to_string(), "Clear Config").into(),
.add_native_item(MenuItem::Minimize) MenuItem::Separator.into(),
.add_native_item(MenuItem::Zoom), CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(),
); ]),
);
let help_menu = Submenu::new(
"Help",
Menu::new()
.add_item(CustomMenuItem::new(
"chatgpt_log".to_string(),
"ChatGPT Log",
))
.add_item(CustomMenuItem::new("update_log".to_string(), "Update Log"))
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
.add_item(
CustomMenuItem::new("dev_tools".to_string(), "Toggle Developer Tools")
.accelerator("CmdOrCtrl+Shift+I"),
),
);
let edit_menu = Submenu::new(
"Edit",
Menu::new() Menu::new()
.add_submenu(app_menu) .add_native_item(MenuItem::Undo)
.add_submenu(preferences_menu) .add_native_item(MenuItem::Redo)
.add_submenu(window_menu) .add_native_item(MenuItem::Separator)
.add_submenu(edit_menu) .add_native_item(MenuItem::Cut)
.add_submenu(view_menu) .add_native_item(MenuItem::Copy)
.add_submenu(help_menu) .add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::SelectAll),
);
let view_menu = Submenu::new(
"View",
Menu::new()
.add_item(CustomMenuItem::new("go_back".to_string(), "Go Back").accelerator("CmdOrCtrl+["))
.add_item(
CustomMenuItem::new("go_forward".to_string(), "Go Forward").accelerator("CmdOrCtrl+]"),
)
.add_item(
CustomMenuItem::new("scroll_top".to_string(), "Scroll to Top of Screen")
.accelerator("CmdOrCtrl+Up"),
)
.add_item(
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen")
.accelerator("CmdOrCtrl+Down"),
)
.add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("zoom_0".to_string(), "Zoom to Actual Size").accelerator("CmdOrCtrl+0"),
)
.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(
"Window",
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_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Minimize)
.add_native_item(MenuItem::Zoom),
);
let help_menu = Submenu::new(
"Help",
Menu::new()
.add_item(CustomMenuItem::new(
"chatgpt_log".to_string(),
"ChatGPT Log",
))
.add_item(CustomMenuItem::new("update_log".to_string(), "Update Log"))
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
.add_item(
CustomMenuItem::new("dev_tools".to_string(), "Toggle Developer Tools")
.accelerator("CmdOrCtrl+Shift+I"),
),
);
Menu::new()
.add_submenu(app_menu)
.add_submenu(preferences_menu)
.add_submenu(window_menu)
.add_submenu(edit_menu)
.add_submenu(view_menu)
.add_submenu(help_menu)
} }
// --- Menu Event // --- Menu Event
pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) { pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
let win = Some(event.window()).unwrap(); let win = Some(event.window()).unwrap();
let app = win.app_handle(); let app = win.app_handle();
let script_path = utils::script_path().to_string_lossy().to_string(); let script_path = utils::script_path().to_string_lossy().to_string();
let menu_id = event.menu_item_id(); let menu_id = event.menu_item_id();
let menu_handle = win.menu_handle(); let menu_handle = win.menu_handle();
match menu_id { match menu_id {
// App // App
"about" => { "about" => {
let tauri_conf = utils::get_tauri_conf().unwrap(); let tauri_conf = utils::get_tauri_conf().unwrap();
tauri::api::dialog::message( tauri::api::dialog::message(
app.get_window("core").as_ref(), app.get_window("core").as_ref(),
"ChatGPT", "ChatGPT",
format!("Version {}", tauri_conf.package.version.unwrap()), format!("Version {}", tauri_conf.package.version.unwrap()),
); );
}
"check_update" => {
utils::run_check_update(app, false, None);
}
// Preferences
"control_center" => window::control_window(&app),
"restart" => tauri::api::process::restart(&app.env()),
"inject_script" => open(&app, script_path),
"go_conf" => utils::open_file(utils::chat_root()),
"clear_conf" => utils::clear_conf(&app),
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
"buy_coffee" => open(&app, conf::BUY_COFFEE.to_string()),
"popup_search" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
let popup_search = !chat_conf.popup_search;
menu_handle
.get_item(menu_id)
.set_selected(popup_search)
.unwrap();
ChatConfJson::amend(&serde_json::json!({ "popup_search": popup_search }), None)
.unwrap();
cmd::window_reload(app.clone(), "core");
cmd::window_reload(app, "tray");
}
"sync_prompts" => {
tauri::api::dialog::ask(
app.get_window("core").as_ref(),
"Sync Prompts",
"Data sync will enable all prompts, are you sure you want to sync?",
move |is_restart| {
if is_restart {
app.get_window("core")
.unwrap()
.eval("window.__sync_prompts && window.__sync_prompts()")
.unwrap()
}
},
);
}
"hide_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap()
}
"titlebar" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
ChatConfJson::amend(
&serde_json::json!({ "titlebar": !chat_conf.titlebar }),
None,
)
.unwrap();
tauri::api::process::restart(&app.env());
}
"system_tray" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
ChatConfJson::amend(&serde_json::json!({ "tray": !chat_conf.tray }), None).unwrap();
tauri::api::process::restart(&app.env());
}
"theme_light" | "theme_dark" | "theme_system" => {
let theme = match menu_id {
"theme_dark" => "Dark",
"theme_system" => "System",
_ => "Light",
};
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap();
}
"update_prompt" | "update_silent" | "update_disable" => {
// for id in ["update_prompt", "update_silent", "update_disable"] {
for id in ["update_prompt", "update_silent"] {
menu_handle.get_item(id).set_selected(false).unwrap();
}
let auto_update = match menu_id {
"update_silent" => {
menu_handle
.get_item("update_silent")
.set_selected(true)
.unwrap();
"Silent"
}
"update_disable" => {
menu_handle
.get_item("update_disable")
.set_selected(true)
.unwrap();
"Disable"
}
_ => {
menu_handle
.get_item("update_prompt")
.set_selected(true)
.unwrap();
"Prompt"
}
};
ChatConfJson::amend(&serde_json::json!({ "auto_update": auto_update }), None).unwrap();
}
"stay_on_top" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
let stay_on_top = !chat_conf.stay_on_top;
menu_handle
.get_item(menu_id)
.set_selected(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();
}
// Window
"dalle2" => window::dalle2_window(&app, None, None, Some(false)),
// View
"reload" => win.eval("window.location.reload()").unwrap(),
"go_back" => 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
.eval(
r#"window.scroll({
top: 0,
left: 0,
behavior: "smooth"
})"#,
)
.unwrap(),
"scroll_bottom" => win
.eval(
r#"window.scroll({
top: document.body.scrollHeight,
left: 0,
behavior: "smooth"})"#,
)
.unwrap(),
// Help
"chatgpt_log" => utils::open_file(utils::chat_root().join("chatgpt.log")),
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
"report_bug" => open(&app, conf::ISSUES_URL.to_string()),
"dev_tools" => {
win.open_devtools();
win.close_devtools();
}
_ => (),
} }
"check_update" => {
utils::run_check_update(app, false, None);
}
// Preferences
"control_center" => window::cmd::control_window(app),
"restart" => tauri::api::process::restart(&app.env()),
"inject_script" => open(&app, script_path),
"go_conf" => utils::open_file(utils::app_root()),
"clear_conf" => utils::clear_conf(&app),
"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()),
"popup_search" => {
let app_conf = AppConf::read();
let popup_search = !app_conf.popup_search;
menu_handle
.get_item(menu_id)
.set_selected(popup_search)
.unwrap();
app_conf
.amend(serde_json::json!({ "popup_search": popup_search }))
.write();
window::cmd::window_reload(app.clone(), "core");
window::cmd::window_reload(app, "tray");
}
"sync_prompts" => {
tauri::api::dialog::ask(
app.get_window("core").as_ref(),
"Sync Prompts",
"Data sync will enable all prompts, are you sure you want to sync?",
move |is_restart| {
if is_restart {
app
.get_window("core")
.unwrap()
.eval("window.__sync_prompts && window.__sync_prompts()")
.unwrap()
}
},
);
}
"hide_dock_icon" => {
AppConf::read()
.amend(serde_json::json!({ "hide_dock_icon": true }))
.write()
.restart(app);
}
"titlebar" => {
let app_conf = AppConf::read();
app_conf
.clone()
.amend(serde_json::json!({ "titlebar": !app_conf.titlebar }))
.write()
.restart(app);
}
"system_tray" => {
let app_conf = AppConf::read();
app_conf
.clone()
.amend(serde_json::json!({ "tray": !app_conf.tray }))
.write()
.restart(app);
}
"theme_light" | "theme_dark" | "theme_system" => {
let theme = match menu_id {
"theme_dark" => "dark",
"theme_system" => "system",
_ => "light",
};
AppConf::read()
.amend(serde_json::json!({ "theme": theme }))
.write()
.restart(app);
}
"update_prompt" | "update_silent" | "update_disable" => {
// for id in ["update_prompt", "update_silent", "update_disable"] {
for id in ["update_prompt", "update_silent"] {
menu_handle.get_item(id).set_selected(false).unwrap();
}
let auto_update = match menu_id {
"update_silent" => {
menu_handle
.get_item("update_silent")
.set_selected(true)
.unwrap();
"silent"
}
"update_disable" => {
menu_handle
.get_item("update_disable")
.set_selected(true)
.unwrap();
"disable"
}
_ => {
menu_handle
.get_item("update_prompt")
.set_selected(true)
.unwrap();
"prompt"
}
};
AppConf::read()
.amend(serde_json::json!({ "auto_update": auto_update }))
.write();
}
"stay_on_top" => {
let app_conf = AppConf::read();
let stay_on_top = !app_conf.stay_on_top;
menu_handle
.get_item(menu_id)
.set_selected(stay_on_top)
.unwrap();
win.set_always_on_top(stay_on_top).unwrap();
app_conf
.amend(serde_json::json!({ "stay_on_top": stay_on_top }))
.write();
}
// Window
"dalle2" => window::dalle2_window(&app, None, None, Some(false)),
// 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(),
"go_back" => win.eval("window.history.go(-1)").unwrap(),
"go_forward" => win.eval("window.history.go(1)").unwrap(),
"scroll_top" => win
.eval(
r#"window.scroll({
top: 0,
left: 0,
behavior: "smooth"
})"#,
)
.unwrap(),
"scroll_bottom" => win
.eval(
r#"window.scroll({
top: document.body.scrollHeight,
left: 0,
behavior: "smooth"})"#,
)
.unwrap(),
// Help
"chatgpt_log" => utils::open_file(utils::app_root().join("chatgpt.log")),
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
"report_bug" => open(&app, conf::ISSUES_URL.to_string()),
"dev_tools" => {
win.open_devtools();
win.close_devtools();
}
_ => (),
}
} }
// --- 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 {
"show_dock_icon".to_string(), tray_menu = tray_menu.add_item(CustomMenuItem::new(
"Show Dock Icon", "show_dock_icon".to_string(),
)) "Show Dock Icon",
.add_item(CustomMenuItem::new( ));
"hide_dock_icon".to_string(),
"Hide Dock Icon",
))
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
)
} else { } else {
SystemTray::new().with_menu( tray_menu = tray_menu
SystemTrayMenu::new() .add_item(CustomMenuItem::new(
.add_item(CustomMenuItem::new( "hide_dock_icon".to_string(),
"control_center".to_string(), "Hide Dock Icon",
"Control Center", ))
)) .add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"));
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
)
} }
SystemTray::new().with_menu(
tray_menu
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
)
} else {
SystemTray::new().with_menu(
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"control_center".to_string(),
"Control Center",
))
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
)
}
} }
// --- SystemTray Event // --- SystemTray Event
pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) { pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
on_tray_event(handle, &event); on_tray_event(handle, &event);
let app = handle.clone(); let app = handle.clone();
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();
tray_win.move_window(Position::TrayCenter).unwrap();
if tray_win.is_visible().unwrap() {
tray_win.hide().unwrap();
} else {
tray_win.show().unwrap();
}
} }
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { }
"control_center" => window::control_window(&app),
"restart" => tauri::api::process::restart(&handle.env()), if let Some(tray_win) = handle.get_window("tray") {
"show_dock_icon" => { tray_win.move_window(Position::TrayCenter).unwrap();
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app))
.unwrap(); if tray_win.is_visible().unwrap() {
} tray_win.hide().unwrap();
"hide_dock_icon" => { } else {
let chat_conf = conf::ChatConfJson::get_chat_conf(); tray_win.show().unwrap();
if !chat_conf.hide_dock_icon { }
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)) }
.unwrap();
}
}
"show_core" => {
let core_win = app.get_window("core").unwrap();
let tray_win = app.get_window("tray").unwrap();
if !core_win.is_visible().unwrap() {
core_win.show().unwrap();
core_win.set_focus().unwrap();
tray_win.hide().unwrap();
}
}
"quit" => std::process::exit(0),
_ => (),
},
_ => (),
} }
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"control_center" => window::cmd::control_window(app),
"restart" => tauri::api::process::restart(&handle.env()),
"show_dock_icon" => {
AppConf::read()
.amend(serde_json::json!({ "hide_dock_icon": false }))
.write()
.restart(app);
}
"hide_dock_icon" => {
let app_conf = AppConf::read();
if !app_conf.hide_dock_icon {
app_conf
.amend(serde_json::json!({ "hide_dock_icon": true }))
.write()
.restart(app);
}
}
"show_core" => {
if let Some(core_win) = app.get_window("core") {
let tray_win = app.get_window("tray").unwrap();
if !core_win.is_visible().unwrap() {
core_win.show().unwrap();
core_win.set_focus().unwrap();
tray_win.hide().unwrap();
}
};
}
"quit" => std::process::exit(0),
_ => (),
},
_ => (),
}
} }
pub fn open(app: &AppHandle, path: String) { pub fn open(app: &AppHandle, path: String) {
tauri::api::shell::open(&app.shell_scope(), path, None).unwrap(); tauri::api::shell::open(&app.shell_scope(), path, None).unwrap();
} }

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,113 +1,106 @@
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 {
info!("stepup_tray");
window::tray_window(&handle);
});
if let Some(v) = app_conf.clone().global_shortcut {
info!("global_shortcut: `{}`", v);
match v.parse::<Accelerator>() {
Ok(_) => {
info!("global_shortcut_register");
let handle = app.app_handle();
let mut shortcut = app.global_shortcut_manager();
shortcut
.register(&v, move || {
if let Some(w) = handle.get_window("core") {
if w.is_visible().unwrap() {
w.hide().unwrap();
} else {
w.show().unwrap();
w.set_focus().unwrap();
}
}
})
.unwrap_or_else(|err| {
error!("global_shortcut_register_error: {}", err);
});
}
Err(err) => {
error!("global_shortcut_parse_error: {}", err);
}
}
} else {
info!("global_shortcut_unregister");
};
let app_conf2 = app_conf.clone();
if app_conf.hide_dock_icon {
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
} else {
let app = app.handle();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
window::tray_window(&handle); let link = if app_conf2.main_dashboard {
"index.html"
} else {
&url
};
info!("main_window: {}", link);
let mut main_win = WindowBuilder::new(&app, "core", WindowUrl::App(link.into()))
.title("ChatGPT")
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0)
.theme(Some(theme))
.always_on_top(app_conf2.stay_on_top)
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../scripts/core.js"))
.user_agent(&app_conf2.ua_window);
#[cfg(target_os = "macos")]
{
main_win = main_win
.title_bar_style(app_conf2.clone().titlebar())
.hidden_title(true);
}
if url == "https://chat.openai.com" && !app_conf2.main_dashboard {
main_win = main_win
.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/popup.core.js"))
.initialization_script(include_str!("../scripts/export.js"))
.initialization_script(include_str!("../scripts/markdown.export.js"))
.initialization_script(include_str!("../scripts/cmd.js"))
}
main_win.build().unwrap();
}); });
}
if let Some(v) = chat_conf.global_shortcut { // auto_update
info!("global_shortcut: `{}`", v); let auto_update = app_conf.get_auto_update();
match v.parse::<Accelerator>() { if auto_update != "disable" {
Ok(_) => { info!("run_check_update");
info!("global_shortcut_register"); let app = app.handle();
let handle = app.app_handle(); utils::run_check_update(app, auto_update == "silent", None);
let mut shortcut = app.global_shortcut_manager(); }
shortcut
.register(&v, move || {
if let Some(w) = handle.get_window("core") {
if w.is_visible().unwrap() {
w.hide().unwrap();
} else {
w.show().unwrap();
w.set_focus().unwrap();
}
}
})
.unwrap_or_else(|err| {
info!("global_shortcut_register_error: {}", err);
});
}
Err(err) => {
info!("global_shortcut_parse_error: {}", err);
}
}
} else {
info!("global_shortcut_unregister");
};
if chat_conf.hide_dock_icon { Ok(())
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
} else {
let app = app.handle();
tauri::async_runtime::spawn(async move {
#[cfg(target_os = "macos")]
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
.title("ChatGPT")
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0)
.hidden_title(true)
.theme(theme)
.always_on_top(chat_conf.stay_on_top)
.title_bar_style(ChatConfJson::titlebar())
.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/popup.core.js"))
.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"))]
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
.title("ChatGPT")
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0)
.theme(theme)
.always_on_top(chat_conf.stay_on_top)
.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/popup.core.js"))
.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();
});
}
// auto_update
if chat_conf.auto_update != "Disable" {
info!("stepup::run_check_update");
let app = app.handle();
utils::run_check_update(app, chat_conf.auto_update == "Silent", None);
}
Ok(())
} }

View File

@@ -1,107 +1,172 @@
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 {
.title("ChatGPT") "index.html"
.resizable(false) } else {
.fullscreen(false) &app_conf.tray_origin
.inner_size(360.0, 540.0) };
.decorations(false) let mut tray_win = WindowBuilder::new(&app, "tray", WindowUrl::App(link.into()))
.always_on_top(true) .title("ChatGPT")
.theme(theme) .resizable(false)
.initialization_script(&utils::user_script()) .fullscreen(false)
.initialization_script(include_str!("../vendors/floating-ui-core.js")) .inner_size(360.0, 540.0)
.initialization_script(include_str!("../vendors/floating-ui-dom.js")) .decorations(false)
.initialization_script(include_str!("../scripts/core.js")) .always_on_top(true)
.initialization_script(include_str!("../scripts/cmd.js")) .theme(Some(theme))
.initialization_script(include_str!("../scripts/popup.core.js")) .initialization_script(&utils::user_script())
.user_agent(&chat_conf.ua_tray) .initialization_script(include_str!("../scripts/core.js"))
.build() .user_agent(&app_conf.ua_tray);
.unwrap()
.hide() if app_conf.tray_origin == "https://chat.openai.com" && !app_conf.tray_dashboard {
.unwrap(); tray_win = tray_win
}); .initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
.initialization_script(include_str!("../scripts/cmd.js"))
.initialization_script(include_str!("../scripts/popup.core.js"))
}
tray_win.build().unwrap().hide().unwrap();
});
} }
pub fn dalle2_window( pub fn dalle2_window(
handle: &tauri::AppHandle, handle: &tauri::AppHandle,
query: Option<String>, query: Option<String>,
title: Option<String>, title: Option<String>,
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}})", } else {
query.unwrap() "".to_string()
) };
} else {
"".to_string()
};
let label = if is_new.unwrap_or(true) { let label = if is_new.unwrap_or(true) {
let timestamp = SystemTime::now() let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs(); .as_secs();
format!("dalle2_{}", timestamp) format!("dalle2_{}", timestamp)
} else { } else {
"dalle2".to_string() "dalle2".to_string()
}; };
if app.get_window("dalle2").is_none() { if app.get_window("dalle2").is_none() {
tauri::async_runtime::spawn(async move {
WindowBuilder::new(
&app,
label,
WindowUrl::App("https://labs.openai.com".into()),
)
.title(title.unwrap_or_else(|| "DALL·E 2".to_string()))
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0)
.always_on_top(false)
.theme(theme)
.initialization_script(include_str!("../scripts/core.js"))
.initialization_script(&query)
.initialization_script(include_str!("../scripts/dalle2.js"))
.build()
.unwrap();
});
} else {
let dalle2_win = app.get_window("dalle2").unwrap();
dalle2_win.show().unwrap();
dalle2_win.set_focus().unwrap();
}
}
pub fn control_window(handle: &tauri::AppHandle) {
let app = handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
if app.app_handle().get_window("main").is_none() { WindowBuilder::new(
WindowBuilder::new(&app, "main", WindowUrl::App("index.html".into())) &app,
.title("Control Center") label,
.resizable(true) WindowUrl::App("https://labs.openai.com".into()),
.fullscreen(false) )
.inner_size(800.0, 600.0) .title(title.unwrap_or_else(|| "DALL·E 2".to_string()))
.min_inner_size(800.0, 600.0) .resizable(true)
.build() .fullscreen(false)
.unwrap(); .inner_size(800.0, 600.0)
} else { .always_on_top(false)
let main_win = app.app_handle().get_window("main").unwrap(); .theme(Some(theme))
main_win.show().unwrap(); .initialization_script(include_str!("../scripts/core.js"))
main_win.set_focus().unwrap(); .initialization_script(&query)
} .initialization_script(include_str!("../scripts/dalle2.js"))
.build()
.unwrap();
}); });
} else {
let dalle2_win = app.get_window("dalle2").unwrap();
dalle2_win.show().unwrap();
dalle2_win.set_focus().unwrap();
}
}
pub mod cmd {
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 {
if handle.get_window("main").is_none() {
WindowBuilder::new(
&handle,
"main",
WindowUrl::App("index.html?type=control".into()),
)
.title("Control Center")
.resizable(true)
.fullscreen(false)
.inner_size(1200.0, 700.0)
.min_inner_size(1000.0, 600.0)
.build()
.unwrap();
} else {
let main_win = handle.get_window("main").unwrap();
main_win.show().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,200 +1,219 @@
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": ""
}"#;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub const APP_CONF_PATH: &str = "chat.conf.json";
pub struct ChatConfJson { pub const CHATGPT_URL: &str = "https://chat.openai.com";
// support macOS only 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";
pub titlebar: bool,
pub hide_dock_icon: bool,
// macOS and Windows, Light/Dark/System macro_rules! pub_struct {
pub theme: String, ($name:ident {$($field:ident: $t:ty,)*}) => {
// auto update policy, Prompt/Silent/Disable #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub auto_update: String, pub struct $name {
pub tray: bool, $(pub $field: $t),*
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,
}
}
pub fn file_path() -> PathBuf {
app_root().join(APP_CONF_PATH)
}
pub fn read() -> Self {
match std::fs::read_to_string(Self::file_path()) {
Ok(v) => serde_json::from_str::<AppConf>(&v).unwrap(),
Err(err) => {
error!("conf_read_error: {}", err);
Self::default()
}
}
}
pub fn write(self) -> Self {
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) {
std::fs::write(path, v).unwrap_or_else(|err| {
error!("conf_write: {}", err);
Self::default().write();
});
} else {
error!("conf_ser");
}
self
}
pub fn amend(self, json: Value) -> Self {
let val = serde_json::to_value(&self).unwrap();
let mut config: BTreeMap<String, Value> = serde_json::from_value(val).unwrap();
let new_json: BTreeMap<String, Value> = serde_json::from_value(json).unwrap();
for (k, v) in new_json {
config.insert(k, v);
}
match serde_json::to_string_pretty(&config) {
Ok(v) => match serde_json::from_str::<AppConf>(&v) {
Ok(v) => v,
Err(err) => {
error!("conf_amend_parse: {}", err);
self
} }
},
let conf_file = ChatConfJson::conf_path(); Err(err) => {
let file_content = fs::read_to_string(&conf_file).unwrap(); error!("conf_amend_str: {}", err);
match serde_json::from_str(&file_content) { self
Ok(v) => v, }
Err(err) => {
if err.to_string() == "invalid type: map, expected unit at line 1 column 0" {
return conf_file;
}
fs::write(&conf_file, content).unwrap();
}
};
conf_file
} }
}
pub fn conf_path() -> PathBuf { #[cfg(target_os = "macos")]
chat_root().join("chat.conf.json") pub fn titlebar(self) -> TitleBarStyle {
if self.titlebar {
TitleBarStyle::Transparent
} else {
TitleBarStyle::Overlay
} }
}
pub fn get_chat_conf() -> Self { pub fn theme_mode() -> Theme {
let conf_file = ChatConfJson::conf_path(); match Self::get_theme().as_str() {
let file_content = fs::read_to_string(&conf_file).unwrap(); "system" => match dark_light::detect() {
let content = if cfg!(target_os = "macos") { // Dark mode
DEFAULT_CHAT_CONF_MAC dark_light::Mode::Dark => Theme::Dark,
} else { // Light mode
DEFAULT_CHAT_CONF dark_light::Mode::Light => Theme::Light,
}; // Unspecified
dark_light::Mode::Default => Theme::Light,
match serde_json::from_value(match serde_json::from_str(&file_content) { },
Ok(v) => v, "dark" => Theme::Dark,
Err(_) => { _ => Theme::Light,
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()
}
}
} }
}
pub fn reset_chat_conf() -> Self { pub fn get_theme() -> String {
let conf_file = ChatConfJson::conf_path(); Self::read().theme.to_lowercase()
let content = if cfg!(target_os = "macos") { }
DEFAULT_CHAT_CONF_MAC
} 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 pub fn get_auto_update(self) -> String {
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> { self.auto_update.to_lowercase()
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 { pub fn theme_check(self, mode: &str) -> bool {
config.insert(k, v); self.theme.to_lowercase() == mode
} }
fs::write( pub fn restart(self, app: tauri::AppHandle) {
ChatConfJson::conf_path(), tauri::api::process::restart(&app.env());
serde_json::to_string_pretty(&config)?, }
)?; }
if let Some(handle) = app { impl Default for AppConf {
tauri::api::process::restart(&handle.env()); fn default() -> Self {
// tauri::api::dialog::ask( Self::new()
// handle.get_window("core").as_ref(), }
// "ChatGPT Restart", }
// "Whether to restart immediately?",
// move |is_restart| { pub mod cmd {
// if is_restart { use super::AppConf;
// } use tauri::{command, AppHandle, Manager};
// },
// ); #[command]
} pub fn get_app_conf() -> AppConf {
AppConf::read()
Ok(()) }
}
#[command]
pub fn theme() -> Option<Theme> { pub fn reset_app_conf() -> AppConf {
let conf = ChatConfJson::get_chat_conf(); AppConf::default().write()
let theme = match conf.theme.as_str() { }
"System" => match dark_light::detect() {
// Dark mode #[command]
dark_light::Mode::Dark => Theme::Dark, pub fn get_theme() -> String {
// Light mode AppConf::get_theme()
dark_light::Mode::Light => Theme::Light, }
// Unspecified
dark_light::Mode::Default => Theme::Light, #[command]
}, pub fn form_confirm(_app: AppHandle, data: serde_json::Value) {
"Dark" => Theme::Dark, AppConf::read().amend(serde_json::json!(data)).write();
_ => Theme::Light, }
};
#[command]
Some(theme) 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(
#[cfg(target_os = "macos")] app.app_handle().get_window(label).as_ref(),
pub fn titlebar() -> TitleBarStyle { title,
let conf = ChatConfJson::get_chat_conf(); msg,
if conf.titlebar { move |is_cancel| {
TitleBarStyle::Transparent if is_cancel {
} else { win.close().unwrap();
TitleBarStyle::Overlay }
} },
} );
}
#[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

@@ -1,115 +1,106 @@
#![cfg_attr( #![cfg_attr(
all(not(debug_assertions), target_os = "windows"), all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
mod app; 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 {
error: Color::Red,
warn: Color::Yellow,
debug: Color::Blue,
info: Color::BrightGreen,
trace: Color::Cyan,
};
cmd::download_list("chat.download.json", "download", None, None); gpt::download_list("chat.download.json", "download", None, None);
cmd::download_list("chat.notes.json", "notes", None, None); gpt::download_list("chat.notes.json", "notes", None, None);
let chat_conf = ChatConfJson::get_chat_conf(); 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);
let mut builder = tauri::Builder::default() if cfg!(debug_assertions) {
// https://github.com/tauri-apps/tauri/pull/2736 log = log.with_colors(ColoredLevelConfig {
.plugin( error: Color::Red,
LoggerBuilder::new() warn: Color::Yellow,
.level(log::LevelFilter::Debug) debug: Color::Blue,
.with_colors(colors) info: Color::BrightGreen,
.targets([ trace: Color::Cyan,
// 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_autostart::init(
MacosLauncher::LaunchAgent,
None,
))
.invoke_handler(tauri::generate_handler![
cmd::drag_window,
cmd::fullscreen,
cmd::download,
cmd::save_file,
cmd::open_link,
cmd::get_chat_conf,
cmd::get_theme,
cmd::reset_chat_conf,
cmd::run_check_update,
cmd::form_cancel,
cmd::form_confirm,
cmd::form_msg,
cmd::open_file,
cmd::get_chat_model_cmd,
cmd::parse_prompt,
cmd::sync_prompts,
cmd::sync_user_prompts,
cmd::window_reload,
cmd::dalle2_window,
cmd::cmd_list,
cmd::download_list,
cmd::get_download_list,
fs_extra::metadata,
])
.setup(setup::init)
.menu(menu::init());
if chat_conf.tray { let mut builder = tauri::Builder::default()
builder = builder.system_tray(menu::tray_menu()); .plugin(log.build())
} .plugin(tauri_plugin_positioner::init())
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
None,
))
.invoke_handler(tauri::generate_handler![
cmd::drag_window,
cmd::fullscreen,
cmd::download,
cmd::save_file,
cmd::open_link,
cmd::run_check_update,
cmd::open_file,
cmd::get_data,
gpt::get_chat_model_cmd,
gpt::parse_prompt,
gpt::sync_prompts,
gpt::sync_user_prompts,
gpt::cmd_list,
gpt::download_list,
gpt::get_download_list,
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)
.menu(menu::init());
builder if app_conf.tray {
.on_menu_event(menu::menu_handler) builder = builder.system_tray(menu::tray_menu());
.on_system_tray_event(menu::tray_handler) }
.on_window_event(|event| {
// https://github.com/tauri-apps/tauri/discussions/2684
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
let win = event.window();
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();
// fix: https://github.com/lencx/ChatGPT/issues/93 builder
#[cfg(not(target_os = "macos"))] .on_menu_event(menu::menu_handler)
event.window().hide().unwrap(); .on_system_tray_event(menu::tray_handler)
} else { .on_window_event(|event| {
win.close().unwrap(); if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
} let win = event.window();
api.prevent_close(); if win.label() == "core" {
} event.window().minimize().unwrap();
}) } else {
.run(context) win.close().unwrap();
.expect("error while running ChatGPT application"); }
api.prevent_close();
}
})
.run(context)
.expect("error while running ChatGPT application");
} }

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,19 +66,31 @@ async function init() {
}); });
} }
const _platform = await platform(); if (__TAURI_METADATA__.__currentWindow.label !== 'tray') {
const chatConf = await invoke('get_chat_conf') || {}; const _platform = await platform();
if (/darwin/.test(_platform) && !chatConf.titlebar) { const chatConf = await invoke('get_app_conf') || {};
const topStyleDom = document.createElement("style"); if (/darwin/.test(_platform) && !chatConf.titlebar) {
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;}`; const topStyleDom = document.createElement("style");
document.head.appendChild(topStyleDom); 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;}`;
const topDom = document.createElement("div"); document.head.appendChild(topStyleDom);
topDom.id = "chatgpt-app-window-top"; const topDom = document.createElement("div");
document.body.appendChild(topDom); topDom.id = "chatgpt-app-window-top";
document.body.appendChild(topDom);
topDom.addEventListener("mousedown", () => invoke("drag_window")); if (window.location.host === 'chat.openai.com') {
topDom.addEventListener("touchstart", () => invoke("drag_window")); const nav = document.body.querySelector('nav');
topDom.addEventListener("dblclick", () => invoke("fullscreen")); 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("touchstart", () => invoke("drag_window"));
topDom.addEventListener("dblclick", () => invoke("fullscreen"));
}
} }
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
@@ -77,20 +101,76 @@ async function init() {
} }
}); });
document.addEventListener('wheel', function(event) { if (window.location.host === 'chat.openai.com') {
const deltaX = event.wheelDeltaX; window.__sync_prompts = async function() {
if (Math.abs(deltaX) >= 50) { await invoke('sync_prompts', { time: Date.now() });
if (deltaX > 0) {
window.history.go(-1);
} else {
window.history.go(1);
}
} }
});
window.__sync_prompts = async function() {
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 (

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,217 +1,215 @@
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::{
collections::HashMap, collections::HashMap,
fs::{self, File}, fs::{self, File},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command, process::Command,
}; };
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)
} }
pub fn exists(path: &Path) -> bool { pub fn exists(path: &Path) -> bool {
Path::new(path).exists() Path::new(path).exists()
} }
pub fn create_file(path: &Path) -> Result<File> { pub fn create_file(path: &Path) -> Result<File> {
if let Some(p) = path.parent() { if let Some(p) = path.parent() {
fs::create_dir_all(p)? fs::create_dir_all(p)?
} }
File::create(path).map_err(Into::into) File::create(path).map_err(Into::into)
} }
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();
} }
} }
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
} }
pub fn user_script() -> String { pub fn user_script() -> String {
let user_script_content = fs::read_to_string(script_path()).unwrap_or_else(|_| "".to_string()); let user_script_content = fs::read_to_string(script_path()).unwrap_or_else(|_| "".to_string());
format!( format!(
"window.addEventListener('DOMContentLoaded', function() {{\n{}\n}})", "window.addEventListener('DOMContentLoaded', function() {{\n{}\n}})",
user_script_content user_script_content
) )
} }
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());
#[cfg(target_os = "macos")] info!("open_file: {}", pathname);
Command::new("open").arg("-R").arg(path).spawn().unwrap(); #[cfg(target_os = "macos")]
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
tauri::api::dialog::ask( Are you sure you want to clear all ChatGPT configurations? Performing this operation data can not be restored, please back up in advance.\n
app.get_window("core").as_ref(), Note: The application will exit automatically after the configuration cleanup!",
"Clear Config", root.to_string_lossy()
msg, );
move |is_ok| { tauri::api::dialog::ask(
if is_ok { app.get_window("core").as_ref(),
fs::remove_dir_all(root).unwrap(); "Clear Config",
tauri::api::process::restart(&app2.env()); msg,
} move |is_ok| {
}, if is_ok {
); fs::remove_dir_all(root).unwrap();
std::process::exit(0);
}
},
);
} }
pub fn merge(v: &Value, fields: &HashMap<String, Value>) -> Value { pub fn merge(v: &Value, fields: &HashMap<String, Value>) -> Value {
match v { match v {
Value::Object(m) => { Value::Object(m) => {
let mut m = m.clone(); let mut m = m.clone();
for (k, v) in fields { for (k, v) in fields {
m.insert(k.clone(), v.clone()); m.insert(k.clone(), v.clone());
} }
Value::Object(m) Value::Object(m)
}
v => v.clone(),
} }
v => v.clone(),
}
} }
pub fn gen_cmd(name: String) -> String { pub fn gen_cmd(name: String) -> String {
let re = Regex::new(r"[^a-zA-Z0-9]").unwrap(); let re = Regex::new(r"[^a-zA-Z0-9]").unwrap();
re.replace_all(&name, "_").to_lowercase() re.replace_all(&name, "_").to_lowercase()
} }
pub async fn get_data( pub async fn get_data(
url: &str, url: &str,
app: Option<&tauri::AppHandle>, app: Option<&tauri::AppHandle>,
) -> Result<Option<String>, reqwest::Error> { ) -> Result<Option<String>, reqwest::Error> {
let res = reqwest::get(url).await?; let res = reqwest::get(url).await?;
let is_ok = res.status() == 200; let is_ok = res.status() == 200;
let body = res.text().await?; let body = res.text().await?;
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);
}
Ok(None)
} }
Ok(None)
}
} }
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 { silent_install(app, update_resp).await.unwrap();
silent_install(app, update_resp).await.unwrap(); });
}); } else {
} else { tauri::async_runtime::spawn(async move {
tauri::async_runtime::spawn(async move { prompt_for_install(app, update_resp).await.unwrap();
prompt_for_install(app, update_resp).await.unwrap(); });
});
}
} else if let Some(v) = has_msg {
if v {
tauri::api::dialog::message(
app.app_handle().get_window("core").as_ref(),
"ChatGPT",
"Your ChatGPT is up to date",
);
}
} }
}); } else if let Some(v) = has_msg {
if v {
tauri::api::dialog::message(
app.app_handle().get_window("core").as_ref(),
"ChatGPT",
"Your ChatGPT is up to date",
);
}
}
}
});
} }
// Copy private api in tauri/updater/mod.rs. TODO: refactor to public api // Copy private api in tauri/updater/mod.rs. TODO: refactor to public api
// Prompt a dialog asking if the user want to install the new version // Prompt a dialog asking if the user want to install the new version
// Maybe we should add an option to customize it in future versions. // Maybe we should add an option to customize it in future versions.
pub async fn prompt_for_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> { pub async fn prompt_for_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> {
info!("prompt_for_install"); info!("prompt_for_install");
let windows = app.windows(); let windows = app.windows();
let parent_window = windows.values().next(); let parent_window = windows.values().next();
let package_info = app.package_info().clone(); let package_info = app.package_info().clone();
let body = update.body().unwrap(); let body = update.body().unwrap();
// todo(lemarier): We should review this and make sure we have // todo(lemarier): We should review this and make sure we have
// something more conventional. // something more conventional.
let should_install = tauri::api::dialog::blocking::ask( let should_install = tauri::api::dialog::blocking::ask(
parent_window, parent_window,
format!(r#"A new version of {} is available! "#, package_info.name), format!(r#"A new version of {} is available! "#, package_info.name),
format!( format!(
r#"{} {} is now available -- you have {}. r#"{} {} is now available -- you have {}.
Would you like to install it now? Would you like to install it now?
Release Notes: Release Notes:
{}"#, {}"#,
package_info.name, package_info.name,
update.latest_version(), update.latest_version(),
package_info.version, package_info.version,
body body
), ),
); );
if should_install {
// Launch updater download process
// macOS we display the `Ready to restart dialog` asking to restart
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
update.download_and_install().await?;
// Ask user if we need to restart the application
let should_exit = tauri::api::dialog::blocking::ask(
parent_window,
"Ready to Restart",
"The installation was successful, do you want to restart the application now?",
);
if should_exit {
app.restart();
}
}
Ok(())
}
pub async fn silent_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> {
info!("silent_install");
let windows = app.windows();
let parent_window = windows.values().next();
if should_install {
// Launch updater download process // Launch updater download process
// macOS we display the `Ready to restart dialog` asking to restart // macOS we display the `Ready to restart dialog` asking to restart
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here) // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
@@ -220,33 +218,58 @@ pub async fn silent_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) ->
// Ask user if we need to restart the application // Ask user if we need to restart the application
let should_exit = tauri::api::dialog::blocking::ask( let should_exit = tauri::api::dialog::blocking::ask(
parent_window, parent_window,
"Ready to Restart", "Ready to Restart",
"The silent installation was successful, do you want to restart the application now?", "The installation was successful, do you want to restart the application now?",
); );
if should_exit { if should_exit {
app.restart(); app.restart();
} }
}
Ok(()) Ok(())
}
pub async fn silent_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) -> Result<()> {
info!("silent_install");
let windows = app.windows();
let parent_window = windows.values().next();
// Launch updater download process
// macOS we display the `Ready to restart dialog` asking to restart
// Windows is closing the current App and launch the downloaded MSI when ready (the process stop here)
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
update.download_and_install().await?;
// Ask user if we need to restart the application
let should_exit = tauri::api::dialog::blocking::ask(
parent_window,
"Ready to Restart",
"The silent installation was successful, do you want to restart the application now?",
);
if should_exit {
app.restart();
}
Ok(())
} }
pub fn is_hidden(entry: &walkdir::DirEntry) -> bool { pub fn is_hidden(entry: &walkdir::DirEntry) -> bool {
entry entry
.file_name() .file_name()
.to_str() .to_str()
.map(|s| s.starts_with('.')) .map(|s| s.starts_with('.'))
.unwrap_or(false) .unwrap_or(false)
} }
pub fn vec_to_hashmap( pub fn vec_to_hashmap(
vec: impl Iterator<Item = serde_json::Value>, vec: impl Iterator<Item = serde_json::Value>,
key: &str, key: &str,
map: &mut HashMap<String, serde_json::Value>, map: &mut HashMap<String, serde_json::Value>,
) { ) {
for v in vec { for v in vec {
if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) { if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) {
map.insert(kval.to_string(), v); map.insert(kval.to_string(), v);
}
} }
}
} }

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

@@ -15,12 +15,12 @@ export default function useChatModel(key: string, file = CHAT_MODEL_JSON) {
setModelJson(data); setModelJson(data);
}); });
const modelSet = async (data: Record<string, any>[]|Record<string, any>) => { const modelSet = async (data: Record<string, any>[] | Record<string, any>) => {
const oData = clone(modelJson); const oData = clone(modelJson);
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

@@ -5,7 +5,7 @@ import { DISABLE_AUTO_COMPLETE } from '@/utils';
export default function useColumns(columns: any[] = []) { export default function useColumns(columns: any[] = []) {
const [opType, setOpType] = useState(''); const [opType, setOpType] = useState('');
const [opRecord, setRecord] = useState<Record<string|symbol, any> | null>(null); const [opRecord, setRecord] = useState<Record<string | symbol, any> | null>(null);
const [opTime, setNow] = useState<number | null>(null); const [opTime, setNow] = useState<number | null>(null);
const [opExtra, setExtra] = useState<any>(null); const [opExtra, setExtra] = useState<any>(null);
@@ -58,26 +58,26 @@ 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 onChange={handleChange}
onChange={handleChange} {...DISABLE_AUTO_COMPLETE}
{...DISABLE_AUTO_COMPLETE} onPressEnter={handleSave}
onPressEnter={handleSave} />
/> ) : (
) <div className="rowedit" onClick={handleEdit}>
: ( {val}
<div className='rowedit' onClick={handleEdit}>{val}</div> </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;

135
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,77 +13,106 @@ 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 }}>
<Layout style={{ minHeight: '100vh' }} hasSider> {isDashboard ? (
<Sider <Routes />
theme={isDark ? "dark" : "light"} ) : (
collapsible <Layout style={{ minHeight: '100vh' }} hasSider>
collapsed={collapsed} <Sider
onCollapse={(value) => setCollapsed(value)} theme={isDark ? 'dark' : 'light'}
style={{ collapsible
overflow: 'auto', collapsed={collapsed}
height: '100vh', onCollapse={(value) => setCollapsed(value)}
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
zIndex: 999,
}}
>
<div className="chat-logo"><img src="/logo.png" /></div>
<div className="chat-info">
<Tag>{appInfo.appName}</Tag>
<Tag>
<span style={{ marginRight: 5 }}>{appInfo.appVersion}</span>
<Tooltip title="click to check update">
<a onClick={checkAppUpdate}><SyncOutlined /></a>
</Tooltip>
</Tag>
</div>
<Menu
defaultSelectedKeys={[location.pathname]}
mode="inline"
theme={ appInfo.appTheme === "dark" ? "dark" : "light" }
inlineIndent={12}
items={menuItems}
defaultOpenKeys={['/model']}
onClick={(i) => go(i.key)}
/>
</Sider>
<Layout className="chat-layout" style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}>
<Content
className="chat-container"
style={{ style={{
overflow: 'inherit' overflow: 'auto',
height: '100vh',
position: 'fixed',
left: 0,
top: 0,
bottom: 0,
zIndex: 999,
}} }}
> >
<Routes /> <div className="chat-logo">
</Content> <img src="/logo.png" />
<Footer style={{ textAlign: 'center' }}> </div>
<a href="https://github.com/lencx/chatgpt" target="_blank">ChatGPT Desktop Application</a> ©2022 Created by lencx <div className="chat-info">
</Footer> <Tag>{appInfo.appName}</Tag>
<Tag>
<span style={{ marginRight: 5 }}>{appInfo.appVersion}</span>
<Tooltip title="click to check update">
<a onClick={checkAppUpdate}>
<SyncOutlined />
</a>
</Tooltip>
</Tag>
</div>
<Menu
selectedKeys={[menuKey]}
mode="inline"
theme={appInfo.appTheme === 'dark' ? 'dark' : 'light'}
inlineIndent={12}
items={menuItems}
// defaultOpenKeys={['/model']}
onClick={(i) => go(i.key)}
/>
</Sider>
<Layout
className="chat-layout"
style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}
>
<Content
className="chat-container"
style={{
overflow: 'inherit',
}}
>
<Routes />
</Content>
<Footer style={{ textAlign: 'center' }}>
<a href="https://github.com/lencx/chatgpt" target="_blank">
ChatGPT Desktop Application
</a>{' '}
©2022 Created by lencx
</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;
}
}

4
src/main.tsx vendored
View File

@@ -9,8 +9,8 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<StrictMode> <StrictMode>
<Suspense fallback={null}> <Suspense fallback={null}>
<BrowserRouter> <BrowserRouter>
<Layout/> <Layout />
</BrowserRouter> </BrowserRouter>
</Suspense> </Suspense>
</StrictMode> </StrictMode>,
); );

50
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,24 +107,37 @@ 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
?.filter((j) => !j.hideMenu) ?.filter((j) => !j.hideMenu)
?.map((j) => ({ ...j.meta, key: `${i.path}/${j.path}` || ''})), ?.map((j) => ({ ...j.meta, key: `${i.path}/${j.path}` || '' })),
})); }));
export default () => { export default () => {

64
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,26 +41,39 @@ 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(
name: 'ChatGPT', file,
link: 'https://github.com/lencx/ChatGPT', isList
...defaultVal, ? '[]'
}, null, 2)) : JSON.stringify(
{
name: 'ChatGPT',
link: 'https://github.com/lencx/ChatGPT',
...defaultVal,
},
null,
2,
),
);
} }
try { try {
return JSON.parse(await readTextFile(file)); return JSON.parse(await readTextFile(file));
} 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';
@@ -7,7 +13,7 @@ import { DISABLE_AUTO_COMPLETE, chatRoot } from '@/utils';
import useInit from '@/hooks/useInit'; import useInit from '@/hooks/useInit';
interface SyncFormProps { interface SyncFormProps {
record?: Record<string|symbol, any> | null; record?: Record<string | symbol, any> | null;
type: string; type: string;
} }
@@ -54,10 +60,18 @@ const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record,
const jsonTip = ( const jsonTip = (
<Tooltip <Tooltip
title={<pre>{JSON.stringify([ title={
{ cmd: '', act: '', prompt: '' }, <pre>
{ cmd: '', act: '', prompt: '' }, {JSON.stringify(
], null, 2)}</pre>} [
{ cmd: '', act: '', prompt: '' },
{ cmd: '', act: '', prompt: '' },
],
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,23 +92,19 @@ 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"
addonBefore={pathOptions} addonBefore={pathOptions}
@@ -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',
@@ -36,7 +36,7 @@ export const syncColumns = () => [
render: (v: number) => ( render: (v: number) => (
<div> <div>
<HistoryOutlined style={{ marginRight: 5, color: v ? '#52c41a' : '#ff4d4f' }} /> <HistoryOutlined style={{ marginRight: 5, color: v ? '#52c41a' : '#ff4d4f' }} />
{ v ? fmtDate(v) : ''} {v ? fmtDate(v) : ''}
</div> </div>
), ),
}, },
@@ -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,9 +57,13 @@ 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]);
modelSet(data); modelSet(data);
opInfo.resetRecord(); opInfo.resetRecord();
@@ -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);
hide(); message.success('Data added successfully');
}) }
if (opInfo.opType === 'edit') {
const data = opReplace(opInfo?.opRecord?.[opSafeKey], vals);
modelSet(data);
message.success('Data updated successfully');
}
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

@@ -6,7 +6,7 @@ import Tags from '@comps/Tags';
import { DISABLE_AUTO_COMPLETE } from '@/utils'; import { DISABLE_AUTO_COMPLETE } from '@/utils';
interface UserCustomFormProps { interface UserCustomFormProps {
record?: Record<string|symbol, any> | null; record?: Record<string | symbol, any> | null;
} }
const initFormValue = { const initFormValue = {
@@ -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,50 +63,64 @@ 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
return; ) {
} message.warning(
let data = []; `"cmd: /${vals.cmd}" already exists, please change the "${vals.cmd}" name and resubmit.`,
switch (opInfo.opType) { );
case 'new': data = opAdd(vals); break; return;
case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break; }
default: break; let data = [];
} switch (opInfo.opType) {
await modelCacheSet(data); case 'new':
opInit(data); data = opAdd(vals);
modelSet({ break;
id: 'user_custom', case 'edit':
last_updated: Date.now(), data = opReplace(opInfo?.opRecord?.[opSafeKey], vals);
}); break;
hide(); default:
}) break;
}
await modelCacheSet(data);
opInit(data);
modelSet({
id: 'user_custom',
last_updated: Date.now(),
});
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,
}, },