Compare commits

...

77 Commits

Author SHA1 Message Date
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
59c5a9932d v0.9.2 2023-01-15 18:19:52 +08:00
lencx
7c0003f823 readme 2023-01-15 18:19:41 +08:00
lencx
a798a8a784 fix: slash command does not work (#207) 2023-01-15 18:02:23 +08:00
lencx
18c6b1e4ad Merge pull request #212 from lencx/dev 2023-01-15 16:43:33 +08:00
lencx
4f03487a53 v0.9.1 2023-01-15 16:22:21 +08:00
lencx
ef3820fad8 release 2023-01-15 16:21:34 +08:00
lencx
f0c635bd3b fix: slash command does not work (#207) 2023-01-15 16:14:37 +08:00
lencx
6d950c09e6 feat: export (#175) 2023-01-15 02:44:33 +08:00
lencx
26bd845a72 Merge pull request #206 from lencx/dev 2023-01-15 02:34:21 +08:00
lencx
96f7e32137 v0.9.0 2023-01-15 02:12:56 +08:00
lencx
3c848b4ded chore: export 2023-01-15 02:12:29 +08:00
lencx
f1a807ed46 chore: export 2023-01-15 01:18:03 +08:00
lencx
ae2c56805c chore: export 2023-01-14 23:31:12 +08:00
lencx
a2fcfa3b89 chore: export 2023-01-13 23:59:38 +08:00
lencx
0eb6c559c6 Merge branch 'main' into dev 2023-01-13 12:20:00 +08:00
lencx
bd5f34b7f9 readme 2023-01-13 12:18:49 +08:00
lencx
042155ffdd readme 2023-01-13 11:57:10 +08:00
lencx
08ef0a2437 fix: export (#192) 2023-01-13 11:47:40 +08:00
lencx
e473268df1 chore: export 2023-01-13 01:18:40 +08:00
lencx
e24fd6a33f merge 2023-01-12 23:19:56 +08:00
lencx
eccee44866 Merge pull request #190 from lencx/fix_export 2023-01-12 21:06:16 +08:00
lencx
7446cfc186 chore: export markdown 2023-01-11 00:40:44 +08:00
lencx
2dfb9bac2a chore: export 2023-01-10 10:45:17 +08:00
lencx
2764219867 feat: config tray (#178) 2023-01-09 13:15:54 +08:00
lencx
e68ab20420 chore: fmt 2023-01-08 21:32:42 +08:00
lencx
c99f4b7633 chore: scripts 2023-01-08 17:04:49 +08:00
lencx
335d3e33ba chore: add jq 2023-01-08 12:28:54 +08:00
92 changed files with 5096 additions and 2573 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

@@ -90,13 +90,13 @@ jobs:
publish_dir: ./updater publish_dir: ./updater
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:
- uses: vedantmgoyal2009/winget-releaser@v1 # - uses: vedantmgoyal2009/winget-releaser@v1
with: # with:
identifier: lencx.ChatGPT # identifier: lencx.ChatGPT
token: ${{ secrets.WINGET_TOKEN }} # token: ${{ secrets.WINGET_TOKEN }}
version: ${{ env.version }} # version: ${{ env.version }}

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
}

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,7 +9,9 @@
![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>
@@ -22,22 +24,23 @@
### Windows ### Windows
- [ChatGPT_0.8.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/ChatGPT_0.8.1_x64_en-US.msi): - [ChatGPT_0.10.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT_0.10.1_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.8.1 winget install --id=lencx.ChatGPT -e --version 0.10.0
``` ```
**注意:如果安装路径和应用名称相同,会导致冲突 ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.8.1))** **注意:如果安装路径和应用名称相同,会导致冲突 ([#142](https://github.com/lencx/ChatGPT/issues/142#issuecomment-0.10.1))**
### Mac ### Mac
- [ChatGPT_0.8.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/ChatGPT_0.8.1_x64.dmg) - [ChatGPT_0.10.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT_0.10.1_x64.dmg)
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/ChatGPT.app.tar.gz) - [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/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
@@ -53,8 +56,8 @@
### Linux ### Linux
- [chat-gpt_0.8.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/chat-gpt_0.8.1_amd64.deb) - [chat-gpt_0.10.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/chat-gpt_0.10.1_amd64.deb)
- [chat-gpt_0.8.1_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/chat-gpt_0.8.1_amd64.AppImage): **工作可靠,`.deb` 运行失败时可以尝试它** - [chat-gpt_0.10.1_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/chat-gpt_0.10.1_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
@@ -70,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`)。 -->
@@ -84,6 +87,7 @@
- 跨平台: `macOS` `Linux` `Windows` - 跨平台: `macOS` `Linux` `Windows`
- 导出 ChatGPT 聊天记录 (支持 PNG, PDF 和生成分享链接) - 导出 ChatGPT 聊天记录 (支持 PNG, PDF 和生成分享链接)
- 主窗口和系统托盘支持自定义 URL将任意网站包装成一个桌面应用
- 应用自动升级通知 - 应用自动升级通知
- 丰富的快捷键 - 丰富的快捷键
- 系统托盘悬浮窗 - 系统托盘悬浮窗
@@ -126,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**)
@@ -199,8 +204,9 @@ Mac 上无法安装,提示开发者未验证,具体可以查看下面给出
#### 预安装 #### 预安装
- [Rust](https://www.rust-lang.org/) - [Rust (必须)](https://www.rust-lang.org/)
- [VS Code](https://code.visualstudio.com/) - [Node.js (必须)](https://nodejs.org/)
- [VS Code (可选)](https://code.visualstudio.com/)
- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
- [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) - [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode)
@@ -224,6 +230,9 @@ yarn dev
yarn build yarn build
``` ```
- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180)
- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182)
## ❤️ 感谢 ## ❤️ 感谢
- 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改 - 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改

View File

@@ -9,7 +9,9 @@
![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_) -->
<!-- [![中文版 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) -->
@@ -24,22 +26,23 @@
### Windows ### Windows
- [ChatGPT_0.8.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/ChatGPT_0.8.1_x64_en-US.msi): Direct download installer - [ChatGPT_0.10.1_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT_0.10.1_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.8.1 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.8.1))** **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.1))**
### Mac ### Mac
- [ChatGPT_0.8.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/ChatGPT_0.8.1_x64.dmg): Direct download installer - [ChatGPT_0.10.1_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/ChatGPT_0.10.1_x64.dmg): Direct download installer
- [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/ChatGPT.app.tar.gz): Download the `.app` installer - [ChatGPT.app.tar.gz](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/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
@@ -55,8 +58,8 @@
### Linux ### Linux
- [chat-gpt_0.8.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/chat-gpt_0.8.1_amd64.deb): Download `.deb` installer, advantage small size, disadvantage poor compatibility - [chat-gpt_0.10.1_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/chat-gpt_0.10.1_amd64.deb): Download `.deb` installer, advantage small size, disadvantage poor compatibility
- [chat-gpt_0.8.1_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.8.1/chat-gpt_0.8.1_amd64.AppImage): Works reliably, you can try it if `.deb` fails to run - [chat-gpt_0.10.1_amd64.AppImage](https://github.com/lencx/ChatGPT/releases/download/v0.10.1/chat-gpt_0.10.1_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 -->
@@ -71,7 +74,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`). -->
@@ -86,7 +89,8 @@ You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt
## ✨ Features ## ✨ Features
- Multi-platform: `macOS` `Linux` `Windows` - Multi-platform: `macOS` `Linux` `Windows`
- Export ChatGPT history (PNG, PDF and Share Link) - 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
@@ -129,6 +133,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**)
@@ -173,6 +178,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
- ... - ...
@@ -207,8 +213,9 @@ It's safe, just a wrapper for [OpenAI ChatGPT](https://chat.openai.com) website,
#### PreInstall #### PreInstall
- [Rust](https://www.rust-lang.org/) - [Rust (Required)](https://www.rust-lang.org/)
- [VS Code](https://code.visualstudio.com/) - [Node.js (Required)](https://nodejs.org/)
- [VS Code (Optional)](https://code.visualstudio.com/)
- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
- [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) - [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode)
@@ -232,6 +239,9 @@ yarn dev
yarn build yarn build
``` ```
- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180)
- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182)
## ❤️ Thanks ## ❤️ Thanks
- The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications. - The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications.

View File

@@ -1,160 +1,220 @@
# UPDATE LOG # UPDATE LOG
## 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
Fix: Slash command does not work
## v0.9.1
Fix: Slash command does not work
## v0.9.0
Fix:
- Export button does not work
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.
## 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

@@ -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,13 +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-resizable-panels": "^0.0.33",
"react-router-dom": "^6.4.5", "react-router-dom": "^6.4.5",
"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": {
@@ -50,8 +63,12 @@
"@types/node": "^18.7.10", "@types/node": "^18.7.10",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.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"

6
scripts/download.js vendored
View File

@@ -13,7 +13,9 @@ async function rewrite(filename) {
flag = true; flag = true;
} }
if (flag) { if (flag) {
content[i] = content[i].replace(/(\d+).(\d+).(\d+)/g, argv[0]); if (!/winget install --id=lencx.ChatGPT -e --version/.test(content[i])) {
content[i] = content[i].replace(/(\d+).(\d+).(\d+)/g, argv[0]);
}
} }
if (endRe.test(content[i])) { if (endRe.test(content[i])) {
break; break;
@@ -28,4 +30,4 @@ async function init() {
rewrite('README-ZH_CN.md'); rewrite('README-ZH_CN.md');
} }
init().catch(console.error); init().catch(console.error);

View File

@@ -16,25 +16,22 @@ tauri-build = {version = "1.2.1", features = [] }
[dependencies] [dependencies]
anyhow = "1.0.66" anyhow = "1.0.66"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2.3", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] }
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
log = "0.4.17" log = "0.4.17"
csv = "1.1.6" csv = "1.1.6"
thiserror = "1.0.38" thiserror = "1.0.38"
walkdir = "2.3.2" walkdir = "2.3.2"
regex = "1.7.0" regex = "1.7.0"
tokio = { version = "1.23.0", features = ["macros"] }
reqwest = "0.11.13" reqwest = "0.11.13"
wry = "0.23.4" wry = "0.24.1"
dark-light = "1.0.0" dark-light = "1.0.0"
[dependencies.tauri-plugin-log] serde = { version = "1.0", features = ["derive"] }
git = "https://github.com/lencx/tauri-plugin-log" tokio = { version = "1.23.0", features = ["macros"] }
branch = "dev" tauri = { version = "1.2.4", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] }
features = ["colored"] tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
[dependencies.tauri-plugin-autostart] tauri-plugin-log = { git = "https://github.com/lencx/tauri-plugins-workspace", branch = "dev", features = ["colored"] }
git = "https://github.com/lencx/tauri-plugin-autostart" tauri-plugin-autostart = { git = "https://github.com/lencx/tauri-plugins-workspace", branch = "dev" }
branch = "dev"
# sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite"] }
[features] [features]
# by default Tauri runs in production mode # by default Tauri runs in production mode
@@ -43,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,303 +1,108 @@
use crate::{ use crate::{
app::window, conf::ChatConfJson,
conf::{ChatConfJson, GITHUB_PROMPTS_CSV_URL}, utils::{self, chat_root, create_file},
utils,
}; };
use log::info; use log::info;
use std::{collections::HashMap, fs, path::PathBuf}; use std::{fs, path::PathBuf};
use tauri::{api, command, AppHandle, Manager, Theme}; 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 = api::path::download_dir().unwrap().join(name); let path = chat_root().join(PathBuf::from(name));
fs::write(&path, blob).unwrap(); create_file(&path).unwrap();
utils::open_file(path); fs::write(&path, blob).unwrap();
utils::open_file(path);
}
#[command]
pub fn save_file(_app: AppHandle, name: String, content: String) {
let path = chat_root().join(PathBuf::from(name));
create_file(&path).unwrap();
fs::write(&path, content).unwrap();
utils::open_file(path);
} }
#[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] #[command]
pub fn get_chat_conf() -> ChatConfJson { pub fn get_chat_conf() -> ChatConfJson {
ChatConfJson::get_chat_conf() ChatConfJson::get_chat_conf()
}
#[command]
pub fn get_theme() -> String {
ChatConfJson::theme().unwrap_or(Theme::Light).to_string()
} }
#[command] #[command]
pub fn reset_chat_conf() -> ChatConfJson { pub fn reset_chat_conf() -> ChatConfJson {
ChatConfJson::reset_chat_conf() ChatConfJson::reset_chat_conf()
}
#[command]
pub fn get_theme() -> String {
ChatConfJson::theme().unwrap_or(Theme::Light).to_string()
} }
#[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] #[command]
pub fn form_confirm(_app: AppHandle, data: serde_json::Value) { pub fn form_confirm(_app: AppHandle, data: serde_json::Value) {
ChatConfJson::amend(&serde_json::json!(data), None).unwrap(); ChatConfJson::amend(&serde_json::json!(data), None).unwrap();
} }
#[command] #[command]
pub fn form_cancel(app: AppHandle, label: &str, title: &str, msg: &str) { pub fn form_cancel(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label).unwrap(); let win = app.app_handle().get_window(label).unwrap();
tauri::api::dialog::ask( tauri::api::dialog::ask(
app.app_handle().get_window(label).as_ref(), app.app_handle().get_window(label).as_ref(),
title, title,
msg, msg,
move |is_cancel| { move |is_cancel| {
if is_cancel { if is_cancel {
win.close().unwrap(); win.close().unwrap();
} }
}, },
); );
} }
#[command] #[command]
pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) { pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label); let win = app.app_handle().get_window(label);
tauri::api::dialog::message(win.as_ref(), title, msg); 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>, info!("chatgpt_client_http_error: {}", 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
}
#[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,
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,
} }
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]

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

@@ -0,0 +1,294 @@
use crate::{
app::{fs_extra, window},
conf::GITHUB_PROMPTS_CSV_URL,
utils::{self, chat_root},
};
use log::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::chat_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| {
info!("parse_prompt_error: {}", 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::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::window_reload(app.clone(), "core");
window::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
}

View File

@@ -1,11 +1,11 @@
use crate::{ use crate::{
app::{cmd, window}, app::window,
conf::{self, ChatConfJson}, conf::{self, ChatConfJson},
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,447 +14,458 @@ use tauri::AboutMetadata;
// --- Menu // --- Menu
pub fn init() -> Menu { pub fn init() -> Menu {
let chat_conf = ChatConfJson::get_chat_conf(); let chat_conf = ChatConfJson::get_chat_conf();
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 chat_conf.stay_on_top {
stay_on_top.selected() stay_on_top.selected()
} else { } else {
stay_on_top stay_on_top
}; };
#[cfg(target_os = "macos")] let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
let titlebar = let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B"); let theme_system = CustomMenuItem::new("theme_system".to_string(), "System");
let is_dark = chat_conf.theme == "Dark";
let is_system = chat_conf.theme == "System";
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light"); let update_prompt = CustomMenuItem::new("update_prompt".to_string(), "Prompt");
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark"); let update_silent = CustomMenuItem::new("update_silent".to_string(), "Silent");
let theme_system = CustomMenuItem::new("theme_system".to_string(), "System"); let _update_disable = CustomMenuItem::new("update_disable".to_string(), "Disable");
let is_dark = chat_conf.theme == "Dark";
let is_system = chat_conf.theme == "System";
let update_prompt = CustomMenuItem::new("update_prompt".to_string(), "Prompt"); let popup_search = CustomMenuItem::new("popup_search".to_string(), "Pop-up Search");
let update_silent = CustomMenuItem::new("update_silent".to_string(), "Silent"); let popup_search_menu = if chat_conf.popup_search {
let _update_disable = CustomMenuItem::new("update_disable".to_string(), "Disable"); popup_search.selected()
} else {
popup_search
};
let popup_search = CustomMenuItem::new("popup_search".to_string(), "Pop-up Search"); #[cfg(target_os = "macos")]
let popup_search_menu = if chat_conf.popup_search { let titlebar = CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
popup_search.selected() #[cfg(target_os = "macos")]
} else { let titlebar_menu = if chat_conf.titlebar {
popup_search titlebar.selected()
}; } else {
titlebar
};
#[cfg(target_os = "macos")] let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray");
let titlebar_menu = if chat_conf.titlebar { let system_tray_menu = if chat_conf.tray {
titlebar.selected() system_tray.selected()
} else { } else {
titlebar system_tray
}; };
let preferences_menu = Submenu::new( let preferences_menu = Submenu::new(
"Preferences", "Preferences",
Menu::with_items([ Menu::with_items([
CustomMenuItem::new("control_center".to_string(), "Control Center") CustomMenuItem::new("control_center".to_string(), "Control Center")
.accelerator("CmdOrCtrl+Shift+P") .accelerator("CmdOrCtrl+Shift+P")
.into(), .into(),
MenuItem::Separator.into(), MenuItem::Separator.into(),
stay_on_top_menu.into(), stay_on_top_menu.into(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
titlebar_menu.into(), titlebar_menu.into(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").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(),
]),
);
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 chat_conf.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 chat_conf.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 chat_conf.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("awesome".to_string(), "Awesome ChatGPT").into(),
); 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+Left"))
.add_item(
CustomMenuItem::new("go_forward".to_string(), "Go Forward").accelerator("CmdOrCtrl+Right"),
)
.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("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()),
"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());
}
"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::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();
window::window_reload(app.clone(), "core");
window::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
"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::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();
}
_ => (),
}
} }
// --- 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( SystemTray::new().with_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( .add_item(CustomMenuItem::new(
"show_dock_icon".to_string(), "show_dock_icon".to_string(),
"Show Dock Icon", "Show Dock Icon",
)) ))
.add_item(CustomMenuItem::new( .add_item(CustomMenuItem::new(
"hide_dock_icon".to_string(), "hide_dock_icon".to_string(),
"Hide Dock Icon", "Hide Dock Icon",
)) ))
.add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT")) .add_item(CustomMenuItem::new("show_core".to_string(), "Show ChatGPT"))
.add_native_item(SystemTrayMenuItem::Separator) .add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")), .add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
) )
} else { } else {
SystemTray::new().with_menu( SystemTray::new().with_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_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_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")), .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 chat_conf = conf::ChatConfJson::get_chat_conf();
if !chat_conf.hide_dock_icon { if !chat_conf.hide_dock_icon {
let core_win = handle.get_window("core").unwrap(); let core_win = handle.get_window("core").unwrap();
core_win.minimize().unwrap(); core_win.minimize().unwrap();
} }
let tray_win = handle.get_window("tray").unwrap(); let tray_win = handle.get_window("tray").unwrap();
tray_win.move_window(Position::TrayCenter).unwrap(); tray_win.move_window(Position::TrayCenter).unwrap();
if tray_win.is_visible().unwrap() { if tray_win.is_visible().unwrap() {
tray_win.hide().unwrap(); tray_win.hide().unwrap();
} else { } else {
tray_win.show().unwrap(); tray_win.show().unwrap();
} }
}
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"control_center" => window::control_window(&app),
"restart" => tauri::api::process::restart(&handle.env()),
"show_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app))
.unwrap();
}
"hide_dock_icon" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
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::control_window(app),
"restart" => tauri::api::process::restart(&handle.env()),
"show_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app)).unwrap();
}
"hide_dock_icon" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
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),
_ => (),
},
_ => (),
}
} }
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

@@ -4,104 +4,99 @@ use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, GlobalShortcut
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 chat_conf = ChatConfJson::get_chat_conf();
let url = chat_conf.origin.to_string(); let url = chat_conf.main_origin.to_string();
let theme = ChatConfJson::theme(); let theme = ChatConfJson::theme();
let handle = app.app_handle(); let handle = app.app_handle();
tauri::async_runtime::spawn(async move {
window::tray_window(&handle);
});
if let Some(v) = chat_conf.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| {
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 {
#[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 chat_conf.main_dashboard {
"index.html"
} else {
&url
};
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(theme)
.always_on_top(chat_conf.stay_on_top)
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../scripts/core.js"))
.user_agent(&chat_conf.ua_window);
#[cfg(target_os = "macos")]
{
main_win = main_win
.title_bar_style(ChatConfJson::titlebar())
.hidden_title(true);
}
if url == "https://chat.openai.com" && !chat_conf.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); if chat_conf.auto_update != "Disable" {
match v.parse::<Accelerator>() { info!("stepup::run_check_update");
Ok(_) => { let app = app.handle();
info!("global_shortcut_register"); utils::run_check_update(app, chat_conf.auto_update == "Silent", None);
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| {
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!("../assets/core.js"))
.initialization_script(include_str!("../assets/popup.core.js"))
.initialization_script(include_str!("../assets/export.js"))
.initialization_script(include_str!("../assets/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!("../assets/core.js"))
.initialization_script(include_str!("../assets/popup.core.js"))
.initialization_script(include_str!("../assets/export.js"))
.initialization_script(include_str!("../assets/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

@@ -4,104 +4,164 @@ 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 chat_conf = conf::ChatConfJson::get_chat_conf();
let theme = conf::ChatConfJson::theme(); let theme = conf::ChatConfJson::theme();
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 chat_conf.tray_dashboard {
.title("ChatGPT") "index.html"
.resizable(false) } else {
.fullscreen(false) &chat_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!("../assets/core.js")) .always_on_top(true)
.initialization_script(include_str!("../assets/cmd.js")) .theme(theme)
.initialization_script(include_str!("../assets/popup.core.js")) .initialization_script(&utils::user_script())
.user_agent(&chat_conf.ua_tray) .initialization_script(include_str!("../scripts/core.js"))
.build() .user_agent(&chat_conf.ua_tray);
.unwrap()
.hide() if chat_conf.tray_origin == "https://chat.openai.com" && !chat_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 = conf::ChatConfJson::theme();
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!("../assets/core.js"))
.initialization_script(&query)
.initialization_script(include_str!("../assets/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(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();
}
}
#[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();
}
});
}
#[tauri::command]
pub async 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)
.build()
.unwrap();
});
} else {
if !win.clone().unwrap().is_visible().unwrap() {
win.clone().unwrap().show().unwrap();
}
win
.clone()
.unwrap()
.eval("window.location.reload()")
.unwrap();
win.unwrap().set_focus().unwrap();
}
}
#[tauri::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,280 +0,0 @@
// *** Core Script - CMD ***
function init() {
const styleDom = document.createElement('style');
styleDom.innerHTML = `form {
position: relative;
}
.chat-model-cmd-list {
position: absolute;
bottom: 60px;
max-height: 100px;
overflow: auto;
z-index: 9999;
}
.chat-model-cmd-list>div {
border: solid 2px rgba(80,80,80,.3);
border-radius: 5px;
background-color: #fff;
}
html.dark .chat-model-cmd-list>div {
background-color: #4a4a4a;
}
html.dark .chat-model-cmd-list .cmd-item {
border-color: #666;
}
html.dark .chat-model-cmd-list .cmd-item b {
color: #e8e8e8;
}
html.dark .chat-model-cmd-list .cmd-item i {
color: #999;
}
html.dark .chat-model-cmd-list .cmd-item.selected {
background: rgba(59,130,246,.5);
}
.chat-model-cmd-list .cmd-item {
font-size: 12px;
border-bottom: solid 1px rgba(80,80,80,.2);
padding: 2px 4px;
display: flex;
user-select: none;
cursor: pointer;
}
.chat-model-cmd-list .cmd-item:last-child {
border-bottom: none;
}
.chat-model-cmd-list .cmd-item.selected {
background: rgba(59,130,246,.3);
}
.chat-model-cmd-list .cmd-item b {
display: inline-block;
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: 4px;
margin-right: 10px;
color: #2a2a2a;
}
.chat-model-cmd-list .cmd-item i {
width: 100%;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
color: #888;
}
.chatappico {
width: 20px;
height: 20px;
}
.chatappico.pdf {
width: 24px;
height: 24px;
}
@media screen and (max-width: 767px) {
#download-png-button, #download-pdf-button, #download-html-button {
display: none;
}
}
`;
document.head.append(styleDom);
if (window.formInterval) {
clearInterval(window.formInterval);
}
window.formInterval = setInterval(() => {
const form = document.querySelector("form");
if (!form) return;
clearInterval(window.formInterval);
cmdTip();
}, 200);
}
async function cmdTip() {
const chatModelJson = await invoke('get_chat_model_cmd') || {};
const data = chatModelJson.data;
if (data.length <= 0) return;
const modelDom = document.createElement('div');
modelDom.classList.add('chat-model-cmd-list');
// fix: tray window
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
modelDom.style.bottom = '54px';
}
document.querySelector('form').appendChild(modelDom);
const itemDom = (v) => `<div class="cmd-item" title="${v.prompt}" data-cmd="${v.cmd}" data-prompt="${encodeURIComponent(v.prompt)}"><b title="${v.cmd}">/${v.cmd}</b><i>${v.act}</i></div>`;
const renderList = (v) => {
modelDom.innerHTML = `<div>${v.map(itemDom).join('')}</div>`;
window.__CHAT_MODEL_CMD_PROMPT__ = v[0]?.prompt.trim();
window.__CHAT_MODEL_CMD__ = v[0]?.cmd.trim();
window.__list = modelDom.querySelectorAll('.cmd-item');
window.__index = 0;
window.__list[window.__index].classList.add('selected');
};
const setPrompt = (v = '') => {
if (v.trim()) {
window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${v.trim()}}`);
}
}
const searchInput = document.querySelector('form textarea');
// Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`.
// If more than one command appears in the search results, the first one will be used by default.
searchInput.addEventListener('keydown', (event) => {
if (!window.__CHAT_MODEL_CMD_PROMPT__) {
return;
}
// ------------------ Keyboard scrolling (ArrowUp | ArrowDown) --------------------------
if (event.keyCode === 38 && window.__index > 0) { // ArrowUp
window.__list[window.__index].classList.remove('selected');
window.__index = window.__index - 1;
window.__list[window.__index].classList.add('selected');
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt'));
searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`;
event.preventDefault();
}
if (event.keyCode === 40 && window.__index < window.__list.length - 1) { // ArrowDown
window.__list[window.__index].classList.remove('selected');
window.__index = window.__index + 1;
window.__list[window.__index].classList.add('selected');
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__list[window.__index].getAttribute('data-prompt'));
searchInput.value = `/${window.__list[window.__index].getAttribute('data-cmd')}`;
event.preventDefault();
}
const containerHeight = modelDom.offsetHeight;
const itemHeight = window.__list[0].offsetHeight + 1;
const itemTop = window.__list[window.__index].offsetTop;
const itemBottom = itemTop + itemHeight;
if (itemTop < modelDom.scrollTop || itemBottom > modelDom.scrollTop + containerHeight) {
modelDom.scrollTop = itemTop;
}
// ------------------ TAB key replaces `{q}` tag content -------------------------------
// feat: https://github.com/lencx/ChatGPT/issues/54
if (event.keyCode === 9 && !window.__CHAT_MODEL_STATUS__) {
const strGroup = window.__CHAT_MODEL_CMD_PROMPT__.match(/\{([^{}]*)\}/) || [];
if (strGroup[1]) {
searchInput.value = `/${window.__CHAT_MODEL_CMD__}` + ` {${strGroup[1]}}` + ' |-> ';
window.__CHAT_MODEL_STATUS__ = 1;
}
event.preventDefault();
}
if (window.__CHAT_MODEL_STATUS__ === 1 && event.keyCode === 9) { // TAB
const data = searchInput.value.split('|->');
if (data[1]?.trim()) {
setPrompt(data[1]);
window.__CHAT_MODEL_STATUS__ = 2;
}
event.preventDefault();
}
// input text
if (window.__CHAT_MODEL_STATUS__ === 2 && event.keyCode === 9) { // TAB
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_STATUS__;
event.preventDefault();
}
// ------------------ type in a space to complete the fill ------------------------------------
if (event.keyCode === 32) {
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
}
// ------------------ send --------------------------------------------------------------------
if (event.keyCode === 13 && window.__CHAT_MODEL_CMD_PROMPT__) { // Enter
const data = searchInput.value.split('|->');
setPrompt(data[1]);
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
delete window.__CHAT_MODEL_CMD__;
delete window.__CHAT_MODEL_STATUS__;
event.preventDefault();
}
});
searchInput.addEventListener('input', () => {
if (searchInput.value === '') {
delete window.__CHAT_MODEL_CMD_PROMPT__;
delete window.__CHAT_MODEL_CMD__;
delete window.__CHAT_MODEL_STATUS__;
}
if (window.__CHAT_MODEL_STATUS__) return;
const query = searchInput.value;
if (!query || !/^\//.test(query)) {
modelDom.innerHTML = '';
return;
}
// all cmd result
if (query === '/') {
renderList(data);
return;
}
const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd));
if (result.length > 0) {
renderList(result);
} else {
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
delete window.__CHAT_MODEL_CMD__;
delete window.__CHAT_MODEL_STATUS__;
}
}, {
capture: false,
passive: true,
once: false
});
if (window.searchInterval) {
clearInterval(window.searchInterval);
}
window.searchInterval = setInterval(() => {
// The `chatgpt prompt` fill can be done by clicking on the event.
const searchDom = document.querySelector("form .chat-model-cmd-list>div");
if (!searchDom) return;
searchDom.addEventListener('click', (event) => {
// .cmd-item
const item = event.target.closest("div");
if (item) {
const val = decodeURIComponent(item.getAttribute('data-prompt'));
searchInput.value = val;
document.querySelector('form textarea').focus();
window.__CHAT_MODEL_CMD_PROMPT__ = val;
modelDom.innerHTML = '';
}
}, {
capture: false,
passive: true,
once: false
});
}, 200);
}
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}

View File

@@ -1,103 +0,0 @@
// *** Core Script - IPC ***
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
function transformCallback(callback = () => {}, once = false) {
const identifier = uid();
const prop = `_${identifier}`;
Object.defineProperty(window, prop, {
value: (result) => {
if (once) {
Reflect.deleteProperty(window, prop);
}
return callback(result)
},
writable: false,
configurable: true,
})
return identifier;
}
async function invoke(cmd, args) {
return new Promise((resolve, reject) => {
if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!');
const callback = transformCallback((e) => {
resolve(e);
Reflect.deleteProperty(window, `_${error}`);
}, true)
const error = transformCallback((e) => {
reject(e);
Reflect.deleteProperty(window, `_${callback}`);
}, true)
window.__TAURI_POST_MESSAGE__({
cmd,
callback,
error,
...args
});
});
}
window.uid = uid;
window.invoke = invoke;
window.transformCallback = transformCallback;
async function init() {
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
document.getElementsByTagName('html')[0].style['font-size'] = '70%';
}
if (__TAURI_METADATA__.__currentWindow.label !== 'core') return;
async function platform() {
return invoke('platform', {
__tauriModule: 'Os',
message: { cmd: 'platform' }
});
}
const _platform = await platform();
const chatConf = await invoke('get_chat_conf') || {};
if (/darwin/.test(_platform) && !chatConf.titlebar) {
const topStyleDom = document.createElement("style");
topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`;
document.head.appendChild(topStyleDom);
const topDom = document.createElement("div");
topDom.id = "chatgpt-app-window-top";
document.body.appendChild(topDom);
topDom.addEventListener("mousedown", () => invoke("drag_window"));
topDom.addEventListener("touchstart", () => invoke("drag_window"));
topDom.addEventListener("dblclick", () => invoke("fullscreen"));
}
document.addEventListener("click", (e) => {
const origin = e.target.closest("a");
if (!origin || !origin.target) return;
if (origin && origin.href && origin.target !== '_self') {
invoke('open_link', { url: origin.href });
}
});
document.addEventListener('wheel', function(event) {
const deltaX = event.wheelDeltaX;
if (Math.abs(deltaX) >= 50) {
if (deltaX > 0) {
window.history.go(-1);
} else {
window.history.go(1);
}
}
});
window.__sync_prompts = async function() {
await invoke('sync_prompts', { time: Date.now() });
}
}
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}

View File

@@ -14,183 +14,187 @@ use tauri::TitleBarStyle;
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 AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md";
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#"{ pub const DEFAULT_CHAT_CONF: &str = r#"{
"stay_on_top": false, "stay_on_top": false,
"auto_update": "Prompt", "auto_update": "Prompt",
"theme": "Light", "theme": "Light",
"titlebar": true, "tray": true,
"popup_search": true, "titlebar": true,
"global_shortcut": "", "popup_search": false,
"hide_dock_icon": false, "global_shortcut": "",
"default_origin": "https://chat.openai.com", "hide_dock_icon": false,
"origin": "https://chat.openai.com", "main_dashboard": false,
"ua_window": "", "tray_dashboard": false,
"ua_tray": "" "main_origin": "https://chat.openai.com",
"tray_origin": "https://chat.openai.com",
"default_origin": "https://chat.openai.com",
"ua_window": "",
"ua_tray": "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 DEFAULT_CHAT_CONF_MAC: &str = r#"{ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
"stay_on_top": false, "stay_on_top": false,
"auto_update": "Prompt", "auto_update": "Prompt",
"theme": "Light", "theme": "Light",
"titlebar": false, "tray": true,
"popup_search": true, "titlebar": false,
"global_shortcut": "", "popup_search": false,
"hide_dock_icon": false, "global_shortcut": "",
"default_origin": "https://chat.openai.com", "hide_dock_icon": false,
"origin": "https://chat.openai.com", "main_dashboard": false,
"ua_window": "", "tray_dashboard": false,
"ua_tray": "" "main_origin": "https://chat.openai.com",
"tray_origin": "https://chat.openai.com",
"default_origin": "https://chat.openai.com",
"ua_window": "",
"ua_tray": "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"
}"#; }"#;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ChatConfJson { pub struct ChatConfJson {
// support macOS only // support macOS only
pub titlebar: bool, pub titlebar: bool,
pub hide_dock_icon: bool, pub hide_dock_icon: bool,
// macOS and Windows, Light/Dark/System // macOS and Windows, Light/Dark/System
pub theme: String, pub theme: String,
// auto update policy, Prompt/Silent/Disable // auto update policy, Prompt/Silent/Disable
pub auto_update: String, pub auto_update: String,
pub popup_search: bool, pub tray: bool,
pub stay_on_top: bool, pub popup_search: bool,
pub default_origin: String, pub stay_on_top: bool,
pub origin: String, pub main_dashboard: bool,
pub ua_window: String, pub tray_dashboard: bool,
pub ua_tray: String, pub main_origin: String,
pub global_shortcut: Option<String>, pub tray_origin: String,
pub default_origin: String,
pub ua_window: String,
pub ua_tray: String,
pub global_shortcut: Option<String>,
} }
impl ChatConfJson { impl ChatConfJson {
/// init chat.conf.json /// init chat.conf.json
/// path: ~/.chatgpt/chat.conf.json /// path: ~/.chatgpt/chat.conf.json
pub fn init() -> PathBuf { pub fn init() -> PathBuf {
info!("chat_conf_init"); info!("chat_conf_init");
let conf_file = ChatConfJson::conf_path(); let conf_file = ChatConfJson::conf_path();
let content = if cfg!(target_os = "macos") { let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC DEFAULT_CHAT_CONF_MAC
} else { } else {
DEFAULT_CHAT_CONF DEFAULT_CHAT_CONF
}; };
if !exists(&conf_file) { if !exists(&conf_file) {
create_file(&conf_file).unwrap(); create_file(&conf_file).unwrap();
fs::write(&conf_file, content).unwrap(); fs::write(&conf_file, content).unwrap();
return conf_file; return conf_file;
}
let conf_file = ChatConfJson::conf_path();
let file_content = fs::read_to_string(&conf_file).unwrap();
match serde_json::from_str(&file_content) {
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();
}
};
let conf_file = ChatConfJson::conf_path(); conf_file
let file_content = fs::read_to_string(&conf_file).unwrap(); }
match serde_json::from_str(&file_content) {
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 {
} chat_root().join("chat.conf.json")
}
pub fn conf_path() -> PathBuf { pub fn get_chat_conf() -> Self {
chat_root().join("chat.conf.json") let conf_file = ChatConfJson::conf_path();
} let file_content = fs::read_to_string(&conf_file).unwrap();
let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC
} else {
DEFAULT_CHAT_CONF
};
pub fn get_chat_conf() -> Self { match serde_json::from_value(match serde_json::from_str(&file_content) {
let conf_file = ChatConfJson::conf_path(); Ok(v) => v,
let file_content = fs::read_to_string(&conf_file).unwrap(); Err(_) => {
let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC
} else {
DEFAULT_CHAT_CONF
};
match serde_json::from_value(match serde_json::from_str(&file_content) {
Ok(v) => v,
Err(_) => {
fs::write(&conf_file, content).unwrap();
serde_json::from_str(content).unwrap()
}
}) {
Ok(v) => v,
Err(_) => {
fs::write(&conf_file, content).unwrap();
serde_json::from_value(serde_json::from_str(content).unwrap()).unwrap()
}
}
}
pub fn reset_chat_conf() -> Self {
let conf_file = ChatConfJson::conf_path();
let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC
} else {
DEFAULT_CHAT_CONF
};
fs::write(&conf_file, content).unwrap(); fs::write(&conf_file, content).unwrap();
serde_json::from_str(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 {
let conf_file = ChatConfJson::conf_path();
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 amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> {
let config = ChatConfJson::get_chat_conf();
let config: Value = serde_json::to_value(&config)?;
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
let new_rules: BTreeMap<String, Value> = serde_json::from_value(new_rules.clone())?;
for (k, v) in new_rules {
config.insert(k, v);
} }
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3 fs::write(
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> { ChatConfJson::conf_path(),
let config = ChatConfJson::get_chat_conf(); serde_json::to_string_pretty(&config)?,
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 { if let Some(handle) = app {
config.insert(k, v); tauri::api::process::restart(&handle.env());
}
fs::write(
ChatConfJson::conf_path(),
serde_json::to_string_pretty(&config)?,
)?;
if let Some(handle) = app {
tauri::api::process::restart(&handle.env());
// tauri::api::dialog::ask(
// handle.get_window("core").as_ref(),
// "ChatGPT Restart",
// "Whether to restart immediately?",
// move |is_restart| {
// if is_restart {
// }
// },
// );
}
Ok(())
} }
pub fn theme() -> Option<Theme> { Ok(())
let conf = ChatConfJson::get_chat_conf(); }
let theme = match conf.theme.as_str() {
"System" => match dark_light::detect() {
// Dark mode
dark_light::Mode::Dark => Theme::Dark,
// Light mode
dark_light::Mode::Light => Theme::Light,
// Unspecified
dark_light::Mode::Default => Theme::Light,
},
"Dark" => Theme::Dark,
_ => Theme::Light,
};
Some(theme) pub fn theme() -> Option<Theme> {
} let conf = ChatConfJson::get_chat_conf();
let theme = match conf.theme.as_str() {
"System" => match dark_light::detect() {
// Dark mode
dark_light::Mode::Dark => Theme::Dark,
// Light mode
dark_light::Mode::Light => Theme::Light,
// Unspecified
dark_light::Mode::Default => Theme::Light,
},
"Dark" => Theme::Dark,
_ => Theme::Light,
};
#[cfg(target_os = "macos")] Some(theme)
pub fn titlebar() -> TitleBarStyle { }
let conf = ChatConfJson::get_chat_conf();
if conf.titlebar { #[cfg(target_os = "macos")]
TitleBarStyle::Transparent pub fn titlebar() -> TitleBarStyle {
} else { let conf = ChatConfJson::get_chat_conf();
TitleBarStyle::Overlay if conf.titlebar {
} TitleBarStyle::Transparent
} else {
TitleBarStyle::Overlay
} }
}
} }

View File

@@ -1,102 +1,118 @@
#![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::ChatConfJson;
use tauri::api::path; 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(); ChatConfJson::init();
// 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 { let colors = ColoredLevelConfig {
error: Color::Red, error: Color::Red,
warn: Color::Yellow, warn: Color::Yellow,
debug: Color::Blue, debug: Color::Blue,
info: Color::BrightGreen, info: Color::BrightGreen,
trace: Color::Cyan, trace: Color::Cyan,
}; };
tauri::Builder::default() gpt::download_list("chat.download.json", "download", None, None);
// https://github.com/tauri-apps/tauri/pull/2736 gpt::download_list("chat.notes.json", "notes", None, None);
.plugin(
LoggerBuilder::new() let chat_conf = ChatConfJson::get_chat_conf();
.level(log::LevelFilter::Debug)
.with_colors(colors) let mut builder = tauri::Builder::default()
.targets([ // https://github.com/tauri-apps/tauri/pull/2736
// LogTarget::LogDir, .plugin(
// LOG PATH: ~/.chatgpt/ChatGPT.log tauri_plugin_log::Builder::default()
LogTarget::Folder(path::home_dir().unwrap().join(".chatgpt")), .targets([
LogTarget::Stdout, // LogTarget::LogDir,
LogTarget::Webview, // LOG PATH: ~/.chatgpt/ChatGPT.log
]) LogTarget::Folder(path::home_dir().unwrap().join(".chatgpt")),
.build(), LogTarget::Stdout,
) LogTarget::Webview,
.invoke_handler(tauri::generate_handler![
cmd::drag_window,
cmd::fullscreen,
cmd::download,
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,
fs_extra::metadata,
]) ])
.setup(setup::init) .level(log::LevelFilter::Debug)
.plugin(tauri_plugin_positioner::init()) .with_colors(colors)
.plugin(tauri_plugin_autostart::init( .build(),
MacosLauncher::LaunchAgent, )
None, .plugin(tauri_plugin_positioner::init())
)) .plugin(tauri_plugin_autostart::init(
.menu(menu::init()) MacosLauncher::LaunchAgent,
.system_tray(menu::tray_menu()) None,
.on_menu_event(menu::menu_handler) ))
.on_system_tray_event(menu::tray_handler) .invoke_handler(tauri::generate_handler![
.on_window_event(|event| { cmd::drag_window,
// https://github.com/tauri-apps/tauri/discussions/2684 cmd::fullscreen,
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() { cmd::download,
let win = event.window(); cmd::save_file,
if win.label() == "core" { cmd::open_link,
// TODO: https://github.com/tauri-apps/tauri/issues/3084 cmd::get_chat_conf,
// event.window().hide().unwrap(); cmd::get_theme,
// https://github.com/tauri-apps/tao/pull/517 cmd::reset_chat_conf,
#[cfg(target_os = "macos")] cmd::run_check_update,
event.window().minimize().unwrap(); cmd::form_cancel,
cmd::form_confirm,
cmd::form_msg,
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,
window::wa_window,
window::control_window,
window::window_reload,
window::dalle2_search_window,
fs_extra::metadata,
])
.setup(setup::init)
.menu(menu::init());
// fix: https://github.com/lencx/ChatGPT/issues/93 if chat_conf.tray {
#[cfg(not(target_os = "macos"))] builder = builder.system_tray(menu::tray_menu());
event.window().hide().unwrap(); }
} else {
win.close().unwrap(); builder
} .on_menu_event(menu::menu_handler)
api.prevent_close(); .on_system_tray_event(menu::tray_handler)
} .on_window_event(|event| {
}) // https://github.com/tauri-apps/tauri/discussions/2684
.run(context) if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
.expect("error while running ChatGPT application"); 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
#[cfg(not(target_os = "macos"))]
event.window().hide().unwrap();
} else {
win.close().unwrap();
}
api.prevent_close();
}
})
.run(context)
.expect("error while running ChatGPT application");
} }

296
src-tauri/src/scripts/cmd.js vendored Normal file
View File

@@ -0,0 +1,296 @@
// *** Core Script - CMD ***
function init() {
const styleDom = document.createElement('style');
styleDom.innerHTML = `form {
position: relative;
}
.chat-model-cmd-list {
position: absolute;
bottom: 60px;
max-height: 100px;
overflow: auto;
z-index: 9999;
}
.chat-model-cmd-list>div {
border: solid 2px rgba(80,80,80,.3);
border-radius: 5px;
background-color: #fff;
}
html.dark .chat-model-cmd-list>div {
background-color: #4a4a4a;
}
html.dark .chat-model-cmd-list .cmd-item {
border-color: #666;
}
html.dark .chat-model-cmd-list .cmd-item b {
color: #e8e8e8;
}
html.dark .chat-model-cmd-list .cmd-item i {
color: #999;
}
html.dark .chat-model-cmd-list .cmd-item.selected {
background: rgba(59,130,246,.5);
}
.chat-model-cmd-list .cmd-item {
font-size: 12px;
border-bottom: solid 1px rgba(80,80,80,.2);
padding: 2px 4px;
display: flex;
user-select: none;
cursor: pointer;
}
.chat-model-cmd-list .cmd-item:last-child {
border-bottom: none;
}
.chat-model-cmd-list .cmd-item.selected {
background: rgba(59,130,246,.3);
}
.chat-model-cmd-list .cmd-item b {
display: inline-block;
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: 4px;
margin-right: 10px;
color: #2a2a2a;
}
.chat-model-cmd-list .cmd-item i {
width: 100%;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
color: #888;
}
.chatappico {
width: 20px;
height: 20px;
}
.chatappico.pdf, .chatappico.md {
width: 22px;
height: 22px;
}
@media screen and (max-width: 767px) {
#download-png-button, #download-pdf-button, #download-html-button {
display: none;
}
}
`;
document.head.append(styleDom);
if (window.formInterval) {
clearInterval(window.formInterval);
}
window.formInterval = setInterval(() => {
const form = document.querySelector("form textarea");
if (!form) return;
clearInterval(window.formInterval);
cmdTip();
new MutationObserver(function (mutationsList) {
for (const mutation of mutationsList) {
if (mutation.target.getAttribute('id') === '__next') {
initDom();
cmdTip();
}
if (mutation.target.getAttribute('class') === 'chat-model-cmd-list') {
// The `chatgpt prompt` fill can be done by clicking on the event.
const searchDom = document.querySelector("form .chat-model-cmd-list>div");
const searchInput = document.querySelector('form textarea');
if (!searchDom) return;
searchDom.addEventListener('click', (event) => {
const item = event.target.closest("div");
if (item) {
const val = decodeURIComponent(item.getAttribute('data-prompt'));
searchInput.value = val;
document.querySelector('form textarea').focus();
initDom();
}
});
}
}
}).observe(document.body, {
childList: true,
subtree: true,
});
}, 300);
}
async function cmdTip() {
initDom();
const chatModelJson = await invoke('get_chat_model_cmd') || {};
const data = chatModelJson.data;
if (data.length <= 0) return;
let modelDom = document.querySelector('.chat-model-cmd-list');
if (!modelDom) {
const dom = document.createElement('div');
dom.classList.add('chat-model-cmd-list');
document.querySelector('form').appendChild(dom);
modelDom = document.querySelector('.chat-model-cmd-list');
// fix: tray window
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
modelDom.style.bottom = '54px';
}
const itemDom = (v) => `<div class="cmd-item" title="${v.prompt}" data-cmd="${v.cmd}" data-prompt="${encodeURIComponent(v.prompt)}"><b title="${v.cmd}">/${v.cmd}</b><i>${v.act}</i></div>`;
const renderList = (v) => {
initDom();
modelDom.innerHTML = `<div>${v.map(itemDom).join('')}</div>`;
window.__CHAT_MODEL_CMD_PROMPT__ = v[0]?.prompt.trim();
window.__CHAT_MODEL_CMD__ = v[0]?.cmd.trim();
window.__cmd_list = modelDom.querySelectorAll('.cmd-item');
window.__cmd_index = 0;
window.__cmd_list[window.__cmd_index].classList.add('selected');
};
const setPrompt = (v = '') => {
if (v.trim()) {
window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${v.trim()}}`);
}
}
const searchInput = document.querySelector('form textarea');
// Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`.
// If more than one command appears in the search results, the first one will be used by default.
function cmdKeydown(event) {
if (!window.__CHAT_MODEL_CMD_PROMPT__) {
return;
}
// ------------------ Keyboard scrolling (ArrowUp | ArrowDown) --------------------------
if (event.keyCode === 38 && window.__cmd_index > 0) { // ArrowUp
window.__cmd_list[window.__cmd_index].classList.remove('selected');
window.__cmd_index = window.__cmd_index - 1;
window.__cmd_list[window.__cmd_index].classList.add('selected');
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__cmd_list[window.__cmd_index].getAttribute('data-prompt'));
searchInput.value = `/${window.__cmd_list[window.__cmd_index].getAttribute('data-cmd')}`;
event.preventDefault();
}
if (event.keyCode === 40 && window.__cmd_index < window.__cmd_list.length - 1) { // ArrowDown
window.__cmd_list[window.__cmd_index].classList.remove('selected');
window.__cmd_index = window.__cmd_index + 1;
window.__cmd_list[window.__cmd_index].classList.add('selected');
window.__CHAT_MODEL_CMD_PROMPT__ = decodeURIComponent(window.__cmd_list[window.__cmd_index].getAttribute('data-prompt'));
searchInput.value = `/${window.__cmd_list[window.__cmd_index].getAttribute('data-cmd')}`;
event.preventDefault();
}
const containerHeight = modelDom.offsetHeight;
const itemHeight = window.__cmd_list[0].offsetHeight + 1;
const itemTop = window.__cmd_list[window.__cmd_index].offsetTop;
const itemBottom = itemTop + itemHeight;
if (itemTop < modelDom.scrollTop || itemBottom > modelDom.scrollTop + containerHeight) {
modelDom.scrollTop = itemTop;
}
// ------------------ TAB key replaces `{q}` tag content -------------------------------
// feat: https://github.com/lencx/ChatGPT/issues/54
if (event.keyCode === 9 && !window.__CHAT_MODEL_STATUS__) {
const strGroup = window.__CHAT_MODEL_CMD_PROMPT__.match(/\{([^{}]*)\}/) || [];
if (strGroup[1]) {
searchInput.value = `/${window.__CHAT_MODEL_CMD__}` + ` {${strGroup[1]}}` + ' |-> ';
window.__CHAT_MODEL_STATUS__ = 1;
} else {
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
initDom();
}
event.preventDefault();
}
if (window.__CHAT_MODEL_STATUS__ === 1 && event.keyCode === 9) { // TAB
const data = searchInput.value.split('|->');
if (data[1]?.trim()) {
setPrompt(data[1]);
window.__CHAT_MODEL_STATUS__ = 2;
}
event.preventDefault();
}
// input text
if (window.__CHAT_MODEL_STATUS__ === 2 && event.keyCode === 9) { // TAB
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_STATUS__;
event.preventDefault();
}
// ------------------ type in a space to complete the fill ------------------------------------
if (event.keyCode === 32) {
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD_PROMPT__;
}
// ------------------ send --------------------------------------------------------------------
if (event.keyCode === 13 && window.__CHAT_MODEL_CMD_PROMPT__) { // Enter
const data = searchInput.value.split('|->');
setPrompt(data[1]);
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
initDom();
event.preventDefault();
}
}
searchInput.removeEventListener('keydown', cmdKeydown, { capture: true });
searchInput.addEventListener('keydown', cmdKeydown, { capture: true });
function cmdInput() {
if (searchInput.value === '') {
initDom();
}
if (window.__CHAT_MODEL_STATUS__) return;
const query = searchInput.value;
if (!query || !/^\//.test(query)) {
initDom();
return;
}
// all cmd result
if (query === '/') {
renderList(data);
return;
}
const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd));
if (result.length > 0) {
renderList(result);
} else {
initDom();
}
}
searchInput.removeEventListener('input', cmdInput);
searchInput.addEventListener('input', cmdInput);
}
}
function initDom() {
const modelDom = document.querySelector('.chat-model-cmd-list');
if (modelDom) {
modelDom.innerHTML = '';
}
delete window.__CHAT_MODEL_CMD_PROMPT__;
delete window.__CHAT_MODEL_CMD__;
delete window.__CHAT_MODEL_STATUS__;
delete window.__cmd_list;
delete window.__cmd_index;
}
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}

180
src-tauri/src/scripts/core.js vendored Normal file
View File

@@ -0,0 +1,180 @@
// *** Core Script - IPC ***
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
function transformCallback(callback = () => {}, once = false) {
const identifier = uid();
const prop = `_${identifier}`;
Object.defineProperty(window, prop, {
value: (result) => {
if (once) {
Reflect.deleteProperty(window, prop);
}
return callback(result)
},
writable: false,
configurable: true,
})
return identifier;
}
async function invoke(cmd, args) {
return new Promise((resolve, reject) => {
if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!');
const callback = transformCallback((e) => {
resolve(e);
Reflect.deleteProperty(window, `_${error}`);
}, true)
const error = transformCallback((e) => {
reject(e);
Reflect.deleteProperty(window, `_${callback}`);
}, true)
window.__TAURI_POST_MESSAGE__({
cmd,
callback,
error,
...args
});
});
}
window.uid = uid;
window.invoke = invoke;
window.transformCallback = transformCallback;
async function init() {
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
document.getElementsByTagName('html')[0].style['font-size'] = '70%';
}
async function platform() {
return invoke('platform', {
__tauriModule: 'Os',
message: { cmd: 'platform' }
});
}
if (__TAURI_METADATA__.__currentWindow.label !== 'tray') {
const _platform = await platform();
const chatConf = await invoke('get_chat_conf') || {};
if (/darwin/.test(_platform) && !chatConf.titlebar) {
const topStyleDom = document.createElement("style");
topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`;
document.head.appendChild(topStyleDom);
const topDom = document.createElement("div");
topDom.id = "chatgpt-app-window-top";
document.body.appendChild(topDom);
if (window.location.host === 'chat.openai.com') {
const nav = document.body.querySelector('nav');
if (nav) {
const currentPaddingTop = parseInt(window.getComputedStyle(document.querySelector('nav'), null).getPropertyValue('padding-top').replace('px', ''), 10);
const navStyleDom = document.createElement("style");
navStyleDom.innerHTML = `nav{padding-top:${currentPaddingTop + topDom.clientHeight}px !important}`;
document.head.appendChild(navStyleDom);
}
}
topDom.addEventListener("mousedown", () => invoke("drag_window"));
topDom.addEventListener("touchstart", () => invoke("drag_window"));
topDom.addEventListener("dblclick", () => invoke("fullscreen"));
}
}
document.addEventListener("click", (e) => {
const origin = e.target.closest("a");
if (!origin || !origin.target) return;
if (origin && origin.href && origin.target !== '_self') {
invoke('open_link', { url: origin.href });
}
});
document.addEventListener('wheel', function(event) {
const deltaX = event.wheelDeltaX;
if (Math.abs(deltaX) >= 50) {
if (deltaX > 0) {
window.history.go(-1);
} else {
window.history.go(1);
}
}
});
if (window.location.host === 'chat.openai.com') {
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 (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}

View File

@@ -1,6 +1,6 @@
// *** Core Script - DALL·E 2 *** // *** Core Script - DALL·E 2 ***
async function init() { function init() {
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
const origin = e.target.closest("a"); const origin = e.target.closest("a");
if (!origin || !origin.target) return; if (!origin || !origin.target) return;
@@ -37,4 +37,4 @@ if (
init(); init();
} else { } else {
document.addEventListener("DOMContentLoaded", init); document.addEventListener("DOMContentLoaded", init);
} }

View File

@@ -1,8 +1,7 @@
// *** Core Script - Export *** // *** Core Script - Export ***
// @ref: https://github.com/liady/ChatGPT-pdf
const buttonOuterHTMLFallback = `<button class="btn flex justify-center gap-2 btn-neutral" id="download-png-button">Try Again</button>`;
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>`;
if (window.innerWidth < 767) return; if (window.innerWidth < 767) return;
const chatConf = await invoke('get_chat_conf') || {}; const chatConf = await invoke('get_chat_conf') || {};
if (window.buttonsInterval) { if (window.buttonsInterval) {
@@ -49,14 +48,19 @@ function shouldAddButtons(actionsArea) {
const buttons = actionsArea.querySelectorAll("button"); const buttons = actionsArea.querySelectorAll("button");
const hasTryAgainButton = Array.from(buttons).some((button) => { const hasTryAgainButton = Array.from(buttons).some((button) => {
return !button.id?.includes("download"); return !/download-/.test(button.id);
}); });
// fix: https://github.com/lencx/ChatGPT/issues/189 const stopBtn = buttons?.[0]?.innerText;
if (buttons.length === 1) {
if (/Stop generating/ig.test(stopBtn)) {
return false; return false;
} }
if (buttons.length === 2 && (/Regenerate response/ig.test(stopBtn) || buttons[1].innerText === '')) {
return true;
}
if (hasTryAgainButton && buttons.length === 1) { if (hasTryAgainButton && buttons.length === 1) {
return true; return true;
} }
@@ -80,51 +84,58 @@ function shouldAddButtons(actionsArea) {
function removeButtons() { function removeButtons() {
const downloadButton = document.getElementById("download-png-button"); const downloadButton = document.getElementById("download-png-button");
const downloadPdfButton = document.getElementById("download-pdf-button"); const downloadPdfButton = document.getElementById("download-pdf-button");
const downloadHtmlButton = document.getElementById("download-html-button"); const downloadMdButton = document.getElementById("download-markdown-button");
if (downloadButton) { if (downloadButton) {
downloadButton.remove(); downloadButton.remove();
} }
if (downloadPdfButton) { if (downloadPdfButton) {
downloadPdfButton.remove(); downloadPdfButton.remove();
} }
if (downloadHtmlButton) { if (downloadPdfButton) {
downloadHtmlButton.remove(); downloadMdButton.remove();
} }
} }
function addActionsButtons(actionsArea, TryAgainButton) { function addActionsButtons(actionsArea, TryAgainButton) {
const downloadButton = TryAgainButton.cloneNode(true); const downloadButton = TryAgainButton.cloneNode(true);
// Export markdown
const exportMd = TryAgainButton.cloneNode(true);
exportMd.id = "download-markdown-button";
downloadButton.setAttribute("share-ext", "true");
exportMd.title = "Export Markdown";
exportMd.innerHTML = setIcon('md');
exportMd.onclick = () => {
exportMarkdown();
};
actionsArea.appendChild(exportMd);
// Generate PNG
downloadButton.id = "download-png-button"; downloadButton.id = "download-png-button";
downloadButton.setAttribute("share-ext", "true"); downloadButton.setAttribute("share-ext", "true");
// downloadButton.innerText = "Generate PNG";
downloadButton.title = "Generate PNG"; downloadButton.title = "Generate PNG";
downloadButton.innerHTML = setIcon('png'); downloadButton.innerHTML = setIcon('png');
downloadButton.onclick = () => { downloadButton.onclick = () => {
downloadThread(); downloadThread();
}; };
actionsArea.appendChild(downloadButton); actionsArea.appendChild(downloadButton);
// Generate PDF
const downloadPdfButton = TryAgainButton.cloneNode(true); const downloadPdfButton = TryAgainButton.cloneNode(true);
downloadPdfButton.id = "download-pdf-button"; downloadPdfButton.id = "download-pdf-button";
downloadButton.setAttribute("share-ext", "true"); downloadButton.setAttribute("share-ext", "true");
// downloadPdfButton.innerText = "Download PDF";
downloadPdfButton.title = "Download PDF"; downloadPdfButton.title = "Download PDF";
downloadPdfButton.innerHTML = setIcon('pdf'); downloadPdfButton.innerHTML = setIcon('pdf');
downloadPdfButton.onclick = () => { downloadPdfButton.onclick = () => {
downloadThread({ as: Format.PDF }); downloadThread({ as: Format.PDF });
}; };
actionsArea.appendChild(downloadPdfButton); actionsArea.appendChild(downloadPdfButton);
}
// fix: https://github.com/lencx/ChatGPT/issues/126 async function exportMarkdown() {
// const exportHtml = TryAgainButton.cloneNode(true); const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML);
// exportHtml.id = "download-html-button"; const { id, filename } = getName();
// downloadButton.setAttribute("share-ext", "true"); await invoke('save_file', { name: `notes/${id}.md`, content: data });
// // exportHtml.innerText = "Share Link"; await invoke('download_list', { pathname: 'chat.notes.json', filename, id, dir: 'notes' });
// exportHtml.title = "Share Link";
// exportHtml.innerHTML = setIcon('link');
// exportHtml.onclick = () => {
// sendRequest();
// };
// actionsArea.appendChild(exportHtml);
} }
function downloadThread({ as = Format.PNG } = {}) { function downloadThread({ as = Format.PNG } = {}) {
@@ -150,16 +161,18 @@ function downloadThread({ as = Format.PNG } = {}) {
}); });
} }
function handleImg(imgData) { async function handleImg(imgData) {
const binaryData = atob(imgData.split("base64,")[1]); const binaryData = atob(imgData.split("base64,")[1]);
const data = []; const data = [];
for (let i = 0; i < binaryData.length; i++) { for (let i = 0; i < binaryData.length; i++) {
data.push(binaryData.charCodeAt(i)); data.push(binaryData.charCodeAt(i));
} }
invoke('download', { name: `chatgpt-${Date.now()}.png`, blob: Array.from(new Uint8Array(data)) }); const { pathname, id, filename } = getName();
await invoke('download', { name: `download/img/${id}.png`, blob: data });
await invoke('download_list', { pathname, filename, id, dir: 'download' });
} }
function handlePdf(imgData, canvas, pixelRatio) { async function handlePdf(imgData, canvas, pixelRatio) {
const { jsPDF } = window.jspdf; const { jsPDF } = window.jspdf;
const orientation = canvas.width > canvas.height ? "l" : "p"; const orientation = canvas.width > canvas.height ? "l" : "p";
var pdf = new jsPDF(orientation, "pt", [ var pdf = new jsPDF(orientation, "pt", [
@@ -169,9 +182,16 @@ function handlePdf(imgData, canvas, pixelRatio) {
var pdfWidth = pdf.internal.pageSize.getWidth(); var pdfWidth = pdf.internal.pageSize.getWidth();
var pdfHeight = pdf.internal.pageSize.getHeight(); var pdfHeight = pdf.internal.pageSize.getHeight();
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST'); pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST');
const { pathname, id, filename } = getName();
const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument());
invoke('download', { name: `chatgpt-${Date.now()}.pdf`, blob: Array.from(new Uint8Array(data)) }); await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) });
await invoke('download_list', { pathname, filename, id, dir: 'download' });
}
function getName() {
const id = window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36);
const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || '';
return { filename: name ? name : id, id, pathname: 'chat.download.json' };
} }
class Elements { class Elements {
@@ -187,9 +207,7 @@ class Elements {
// fix: old chat https://github.com/lencx/ChatGPT/issues/185 // fix: old chat https://github.com/lencx/ChatGPT/issues/185
if (!this.thread) { if (!this.thread) {
this.thread = document.querySelector( this.thread = document.querySelector("main .overflow-y-auto");
"main .overflow-y-auto"
);
} }
// h-full overflow-y-auto // h-full overflow-y-auto
@@ -245,54 +263,15 @@ class Elements {
} }
} }
function selectElementByClassPrefix(classPrefix) { function setIcon(type) {
const element = document.querySelector(`[class^='${classPrefix}']`); return {
return element; // 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>`,
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>`
}[type];
} }
async function sendRequest() {
const data = getData();
const uploadUrlResponse = await fetch(
"https://chatgpt-static.s3.amazonaws.com/url.txt"
);
const uploadUrl = await uploadUrlResponse.text();
fetch(uploadUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => response.json())
.then((data) => {
invoke('open_link', { url: data.url });
});
}
function getData() {
const globalCss = getCssFromSheet(
document.querySelector("link[rel=stylesheet]").sheet
);
const localCss =
getCssFromSheet(
document.querySelector(`style[data-styled][data-styled-version]`).sheet
) || "body{}";
const data = {
main: document.querySelector("main").outerHTML,
// css: `${globalCss} /* GLOBAL-LOCAL */ ${localCss}`,
globalCss,
localCss,
};
return data;
}
function getCssFromSheet(sheet) {
return Array.from(sheet.cssRules)
.map((rule) => rule.cssText)
.join("");
}
// run init
if ( if (
document.readyState === "complete" || document.readyState === "complete" ||
document.readyState === "interactive" document.readyState === "interactive"
@@ -301,11 +280,3 @@ if (
} else { } else {
document.addEventListener("DOMContentLoaded", init); document.addEventListener("DOMContentLoaded", init);
} }
function setIcon(type) {
return {
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>`,
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>`
}[type];
}

View File

@@ -0,0 +1,36 @@
var ExportMD = (function () {
if (!TurndownService || !turndownPluginGfm) return;
const hljsREG = /^.*(hljs).*(language-[a-z0-9]+).*$/i;
const gfm = turndownPluginGfm.gfm
const turndownService = new TurndownService()
.use(gfm)
.addRule('code', {
filter: (node) => {
if (node.nodeName === 'CODE' && hljsREG.test(node.classList.value)) {
return 'code';
}
},
replacement: (content, node) => {
const classStr = node.getAttribute('class');
if (hljsREG.test(classStr)) {
const lang = classStr.match(/.*language-(\w+)/)[1];
if (lang) {
return `\`\`\`${lang}\n${content}\n\`\`\``;
}
return `\`\`\`\n${content}\n\`\`\``;
}
}
})
.addRule('ignore', {
filter: ['button', 'img'],
replacement: () => '',
})
.addRule('table', {
filter: 'table',
replacement: function(content, node) {
return `\`\`\`${content}\n\`\`\``;
},
});
return turndownService;
}({}));

View File

@@ -16,9 +16,9 @@ async function init() {
background: #4a4a4a; background: #4a4a4a;
color: white; color: white;
font-weight: bold; font-weight: bold;
padding: 5px 8px; padding: 3px 5px;
border-radius: 4px; border-radius: 2px;
font-size: 12px; font-size: 10px;
cursor: pointer; cursor: pointer;
} }
`; `;
@@ -31,8 +31,9 @@ async function init() {
const { computePosition, flip, offset, shift } = window.FloatingUIDOM; const { computePosition, flip, offset, shift } = window.FloatingUIDOM;
document.body.addEventListener('mousedown', async (e) => { document.body.addEventListener('mousedown', async (e) => {
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__;
} }
@@ -71,7 +72,6 @@ async function init() {
}); });
} }
}); });
} }
if ( if (
@@ -81,4 +81,4 @@ if (
init(); init();
} else { } else {
document.addEventListener("DOMContentLoaded", init); document.addEventListener("DOMContentLoaded", init);
} }

View File

@@ -3,215 +3,213 @@ use log::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 chat_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 = chat_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 = chat_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 = chat_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); info!("chatgpt_http_error: {}", 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; let result = app.updater().check().await;
let update_resp = result.unwrap(); 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 { } else if let Some(v) = has_msg {
if v { if v {
tauri::api::dialog::message( tauri::api::dialog::message(
app.app_handle().get_window("core").as_ref(), app.app_handle().get_window("core").as_ref(),
"ChatGPT", "ChatGPT",
"Your ChatGPT is up to date", "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,13 +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 {
entry
.file_name()
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false)
}
pub fn vec_to_hashmap(
vec: impl Iterator<Item = serde_json::Value>,
key: &str,
map: &mut HashMap<String, serde_json::Value>,
) {
for v in vec {
if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) {
map.insert(kval.to_string(), v);
}
}
} }

View File

@@ -0,0 +1,164 @@
var turndownPluginGfm = (function (exports) {
'use strict';
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
function highlightedCodeBlock (turndownService) {
turndownService.addRule('highlightedCodeBlock', {
filter: function (node) {
var firstChild = node.firstChild;
return (
node.nodeName === 'DIV' &&
highlightRegExp.test(node.className) &&
firstChild &&
firstChild.nodeName === 'PRE'
)
},
replacement: function (content, node, options) {
var className = node.className || '';
var language = (className.match(highlightRegExp) || [null, ''])[1];
return (
'\n\n' + options.fence + language + '\n' +
node.firstChild.textContent +
'\n' + options.fence + '\n\n'
)
}
});
}
function strikethrough (turndownService) {
turndownService.addRule('strikethrough', {
filter: ['del', 's', 'strike'],
replacement: function (content) {
return '~' + content + '~'
}
});
}
var indexOf = Array.prototype.indexOf;
var every = Array.prototype.every;
var rules = {};
rules.tableCell = {
filter: ['th', 'td'],
replacement: function (content, node) {
return cell(content, node)
}
};
rules.tableRow = {
filter: 'tr',
replacement: function (content, node) {
var borderCells = '';
var alignMap = { left: ':--', right: '--:', center: ':-:' };
if (isHeadingRow(node)) {
for (var i = 0; i < node.childNodes.length; i++) {
var border = '---';
var align = (
node.childNodes[i].getAttribute('align') || ''
).toLowerCase();
if (align) border = alignMap[align] || border;
borderCells += cell(border, node.childNodes[i]);
}
}
return '\n' + content + (borderCells ? '\n' + borderCells : '')
}
};
rules.table = {
// Only convert tables with a heading row.
// Tables with no heading row are kept using `keep` (see below).
filter: function (node) {
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
},
replacement: function (content) {
// Ensure there are no blank lines
content = content.replace('\n\n', '\n');
return '\n\n' + content + '\n\n'
}
};
rules.tableSection = {
filter: ['thead', 'tbody', 'tfoot'],
replacement: function (content) {
return content
}
};
// A tr is a heading row if:
// - the parent is a THEAD
// - or if its the first child of the TABLE or the first TBODY (possibly
// following a blank THEAD)
// - and every cell is a TH
function isHeadingRow (tr) {
var parentNode = tr.parentNode;
return (
parentNode.nodeName === 'THEAD' ||
(
parentNode.firstChild === tr &&
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
)
)
}
function isFirstTbody (element) {
var previousSibling = element.previousSibling;
return (
element.nodeName === 'TBODY' && (
!previousSibling ||
(
previousSibling.nodeName === 'THEAD' &&
/^\s*$/i.test(previousSibling.textContent)
)
)
)
}
function cell (content, node) {
var index = indexOf.call(node.parentNode.childNodes, node);
var prefix = ' ';
if (index === 0) prefix = '| ';
return prefix + content + ' |'
}
function tables (turndownService) {
turndownService.keep(function (node) {
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
});
for (var key in rules) turndownService.addRule(key, rules[key]);
}
function taskListItems (turndownService) {
turndownService.addRule('taskListItems', {
filter: function (node) {
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
},
replacement: function (content, node) {
return (node.checked ? '[x]' : '[ ]') + ' '
}
});
}
function gfm (turndownService) {
turndownService.use([
highlightedCodeBlock,
strikethrough,
tables,
taskListItems
]);
}
exports.gfm = gfm;
exports.highlightedCodeBlock = highlightedCodeBlock;
exports.strikethrough = strikethrough;
exports.tables = tables;
exports.taskListItems = taskListItems;
return exports;
}({}));

1
src-tauri/src/vendors/turndown.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@
}, },
"package": { "package": {
"productName": "ChatGPT", "productName": "ChatGPT",
"version": "0.8.1" "version": "0.10.1"
}, },
"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,13 +88,13 @@ 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>
)} )}
</> </>
); );
}; };
export default Tags; export default Tags;

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,15 +40,19 @@ 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' });
}; };
return { modelCacheJson, modelCacheSet, modelCacheCmd }; return { modelCacheJson, modelCacheSet, modelCacheCmd };
} }

View File

@@ -1,44 +0,0 @@
import { useState, useCallback } from 'react';
export default function useColumns(columns: any[] = []) {
const [opType, setOpType] = useState('');
const [opRecord, setRecord] = useState<Record<string|symbol, any> | null>(null);
const [opTime, setNow] = useState<number | null>(null);
const [opExtra, setExtra] = useState<any>(null);
const handleRecord = useCallback((row: Record<string, any> | null, type: string) => {
setOpType(type);
setRecord(row);
setNow(Date.now());
}, []);
const resetRecord = useCallback(() => {
setRecord(null);
setOpType('');
setNow(Date.now());
}, []);
const opNew = useCallback(() => handleRecord(null, 'new'), [handleRecord]);
const cols = columns.map((i: any) => {
if (i.render) {
const opRender = i.render;
i.render = (text: string, row: Record<string, any>) => {
return opRender(text, row, { setRecord: handleRecord, setExtra });
};
}
return i;
});
return {
opTime,
opType,
opNew,
columns: cols,
opRecord,
setRecord: handleRecord,
resetRecord,
setExtra,
opExtra,
};
}

83
src/hooks/useColumns.tsx vendored Normal file
View File

@@ -0,0 +1,83 @@
import { FC, useState, useCallback } from 'react';
import { Input } from 'antd';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
export default function useColumns(columns: any[] = []) {
const [opType, setOpType] = useState('');
const [opRecord, setRecord] = useState<Record<string | symbol, any> | null>(null);
const [opTime, setNow] = useState<number | null>(null);
const [opExtra, setExtra] = useState<any>(null);
const handleRecord = useCallback((row: Record<string, any> | null, type: string) => {
setOpType(type);
setRecord(row);
setNow(Date.now());
}, []);
const resetRecord = useCallback(() => {
setRecord(null);
setOpType('');
setNow(Date.now());
}, []);
const opNew = useCallback(() => handleRecord(null, 'new'), [handleRecord]);
const cols = columns.map((i: any) => {
if (i.render) {
const opRender = i.render;
i.render = (text: string, row: Record<string, any>) => {
return opRender(text, row, { setRecord: handleRecord, setExtra });
};
}
return i;
});
return {
opTime,
opType,
opNew,
columns: cols,
opRecord,
setRecord: handleRecord,
resetRecord,
setExtra,
opExtra,
};
}
interface EditRowProps {
rowKey: string;
row: Record<string, any>;
actions: any;
}
export const EditRow: FC<EditRowProps> = ({ rowKey, row, actions }) => {
const [isEdit, setEdit] = useState(false);
const [val, setVal] = useState(row[rowKey] || '');
const handleEdit = () => {
setEdit(true);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setVal(e.target.value);
};
const handleSave = () => {
setEdit(false);
row[rowKey] = val?.trim();
actions?.setRecord(row, 'rowedit');
};
return isEdit ? (
<Input
value={val}
autoFocus
onChange={handleChange}
{...DISABLE_AUTO_COMPLETE}
onPressEnter={handleSave}
/>
) : (
<div className="rowedit" onClick={handleEdit}>
{val}
</div>
);
};

27
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;
} }
}) });
} }

23
src/hooks/useJson.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import { useState } from 'react';
import { readJSON, writeJSON } from '@/utils';
import useInit from '@/hooks/useInit';
export default function useJson<T>(file: string) {
const [json, setData] = useState<T>();
const refreshJson = async () => {
const data = await readJSON(file);
setData(data);
return data;
};
const updateJson = async (data: any) => {
await writeJSON(file, data);
await refreshJson();
};
useInit(refreshJson);
return { json, refreshJson, updateJson };
}

View File

@@ -4,34 +4,54 @@ import type { TableRowSelection } from 'antd/es/table/interface';
import { safeKey } from '@/hooks/useData'; import { safeKey } from '@/hooks/useData';
export default function useTableRowSelection() { type rowSelectionOptions = {
key: 'id' | string;
rowType: 'id' | 'row' | 'all';
};
export function useTableRowSelection(options: Partial<rowSelectionOptions> = {}) {
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 onSelectChange = (newSelectedRowKeys: React.Key[], selectedRows: Record<string|symbol, any>) => { const onSelectChange = (
const keys = selectedRows.map((i: any) => i[safeKey]); newSelectedRowKeys: React.Key[],
setSelectedRowIDs(keys); newSelectedRows: Record<string | symbol, any>[],
) => {
const keys = newSelectedRows.map((i: any) => i[safeKey] || i[key]);
setSelectedRowKeys(newSelectedRowKeys); setSelectedRowKeys(newSelectedRowKeys);
if (rowType === 'id') {
setSelectedRowIDs(keys);
}
if (rowType === 'row') {
setSelectedRows(newSelectedRows);
}
if (rowType === 'all') {
setSelectedRowIDs(keys);
setSelectedRows(newSelectedRows);
}
};
const rowReset = () => {
setSelectedRowKeys([]);
setSelectedRowIDs([]);
setSelectedRows([]);
}; };
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 }; return { rowSelection, selectedRowIDs, selectedRows, rowReset };
} }
export const TABLE_PAGINATION = { export const TABLE_PAGINATION = {
hideOnSinglePage: true, hideOnSinglePage: true,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
defaultPageSize: 5, defaultPageSize: 10,
pageSizeOptions: [5, 10, 15, 20], pageSizeOptions: [5, 10, 15, 20],
showTotal: (total: number) => <span>Total {total} items</span>, showTotal: (total: number) => <span>Total {total} items</span>,
}; };

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;
@@ -36,4 +40,4 @@
.ant-layout-footer { .ant-layout-footer {
color: #666 !important; color: #666 !important;
opacity: 0.7; opacity: 0.7;
} }

144
src/layout/index.tsx vendored
View File

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

43
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;
@@ -31,14 +35,32 @@ html, body {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
.ellipsis-line {
display: inline-block;
width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rowedit {
padding: 2px 5px;
&:hover {
box-shadow: 0 0 2px rgba(237, 122, 60, 0.8);
border-radius: 4px;
}
}
.chat-add-btn { .chat-add-btn {
margin-bottom: 5px; margin-bottom: 5px;
} }
.chat-tags,
.chat-prompts-tags { .chat-prompts-tags {
.ant-tag { .ant-tag {
margin: 2px; margin: 2px;
@@ -51,11 +73,11 @@ html, body {
} }
} }
.chat-sync-path { .chat-file-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 {
@@ -63,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 {
@@ -77,4 +98,14 @@ html, body {
cursor: pointer; cursor: pointer;
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>,
); );

77
src/routes.tsx vendored
View File

@@ -1,22 +1,32 @@
import { useRoutes } from 'react-router-dom'; import { useRoutes } from 'react-router-dom';
import { import {
DesktopOutlined, SettingOutlined,
BulbOutlined, BulbOutlined,
SyncOutlined, SyncOutlined,
FileSyncOutlined, FileSyncOutlined,
UserOutlined, UserOutlined,
DownloadOutlined,
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 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 = {
@@ -25,17 +35,38 @@ 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: <DesktopOutlined />, icon: <SettingOutlined />,
}, },
}, },
{
path: '/awesome',
element: <Awesome />,
meta: {
label: 'Awesome',
icon: <GlobalOutlined />,
},
},
{
path: '/notes',
element: <Notes />,
meta: {
label: 'Notes',
icon: <FormOutlined />,
},
},
{
path: '/md/:id',
element: <Markdown />,
hideMenu: true,
},
{ {
path: '/model', path: '/model',
meta: { meta: {
@@ -51,6 +82,7 @@ export const routes: Array<ChatRouteObject> = [
icon: <UserOutlined />, icon: <UserOutlined />,
}, },
}, },
// --- Sync
{ {
path: 'sync-prompts', path: 'sync-prompts',
element: <SyncPrompts />, element: <SyncPrompts />,
@@ -72,21 +104,42 @@ export const routes: Array<ChatRouteObject> = [
element: <SyncRecord />, element: <SyncRecord />,
hideMenu: true, hideMenu: true,
}, },
] ],
},
{
path: '/download',
element: <Download />,
meta: {
label: 'Download',
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 () => {
return useRoutes(routes); return useRoutes(routes);
}; };

66
src/utils.ts vendored
View File

@@ -2,29 +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 CHAT_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_AWESOME_JSON = 'chat.awesome.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();
@@ -34,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;
@@ -62,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, CHAT_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(CHAT_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>
);
}

82
src/view/download/config.tsx vendored Normal file
View File

@@ -0,0 +1,82 @@
import { useState } from 'react';
import { Tag, Space, Popconfirm } from 'antd';
import { path, shell } from '@tauri-apps/api';
import { EditRow } from '@/hooks/useColumns';
import useInit from '@/hooks/useInit';
import { fmtDate, chatRoot } from '@/utils';
const colorMap: any = {
pdf: 'blue',
png: 'orange',
};
export const downloadColumns = () => [
{
title: 'Name',
dataIndex: 'name',
fixed: 'left',
key: 'name',
width: 240,
render: (_: string, row: any, actions: any) => (
<EditRow rowKey="name" row={row} actions={actions} />
),
},
{
title: 'Extension',
dataIndex: 'ext',
key: 'ext',
width: 120,
render: (v: string) => <Tag color={colorMap[v]}>{v}</Tag>,
},
{
title: 'Path',
dataIndex: 'path',
key: 'path',
width: 200,
render: (_: string, row: any) => <RenderPath row={row} />,
},
{
title: 'Created',
dataIndex: 'created',
key: 'created',
width: 150,
render: fmtDate,
},
{
title: 'Action',
fixed: 'right',
width: 150,
render: (_: any, row: any, actions: any) => {
return (
<Space>
<a onClick={() => actions.setRecord(row, 'preview')}>Preview</a>
<Popconfirm
title="Are you sure to delete this file?"
onConfirm={() => actions.setRecord(row, 'delete')}
okText="Yes"
cancelText="No"
>
<a>Delete</a>
</Popconfirm>
</Space>
);
},
},
];
const RenderPath = ({ row }: any) => {
const [filePath, setFilePath] = useState('');
useInit(async () => {
setFilePath(await getPath(row));
});
return <a onClick={() => shell.open(filePath)}>{filePath}</a>;
};
export const getPath = async (row: any) => {
const isImg = ['png'].includes(row?.ext);
return (
(await path.join(await chatRoot(), 'download', isImg ? 'img' : row.ext, row.id)) + `.${row.ext}`
);
};

145
src/view/download/index.tsx vendored Normal file
View File

@@ -0,0 +1,145 @@
import { useEffect, useState } from 'react';
import { Table, Modal, Popconfirm, Button, message } from 'antd';
import { invoke, path, fs } 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 { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils';
import { downloadColumns } from './config';
function renderFile(buff: Uint8Array, type: string) {
const renderType = {
pdf: 'application/pdf',
png: 'image/png',
}[type];
return URL.createObjectURL(new Blob([buff], { type: renderType }));
}
export default function Download() {
const [source, setSource] = useState('');
const [isVisible, setVisible] = useState(false);
const { opData, opInit, opReplace, opSafeKey } = useData([]);
const { columns, ...opInfo } = useColumns(downloadColumns());
const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' });
const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_DOWNLOAD_JSON);
const selectedItems = rowSelection.selectedRowKeys || [];
useEffect(() => {
if (!json || json.length <= 0) return;
opInit(json);
}, [json?.length]);
useEffect(() => {
if (!opInfo.opType) return;
(async () => {
const record = opInfo?.opRecord;
const isImg = ['png'].includes(record?.ext);
const file = await path.join(
await chatRoot(),
'download',
isImg ? 'img' : record?.ext,
`${record?.id}.${record?.ext}`,
);
if (opInfo.opType === 'preview') {
const data = await fs.readBinaryFile(file);
const sourceData = renderFile(data, record?.ext);
setSource(sourceData);
setVisible(true);
return;
}
if (opInfo.opType === 'delete') {
await fs.removeFile(file);
await handleRefresh();
}
if (opInfo.opType === 'rowedit') {
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
await updateJson(data);
message.success('Name has been changed!');
}
opInfo.resetRecord();
})();
}, [opInfo.opType]);
const handleDelete = async () => {
if (opData?.length === selectedRows.length) {
const downloadDir = await path.join(await chatRoot(), 'download');
await fs.removeDir(downloadDir, { recursive: true });
await handleRefresh();
message.success('All files have been cleared!');
return;
}
const rows = selectedRows.map(async (i) => {
const isImg = ['png'].includes(i?.ext);
const file = await path.join(
await chatRoot(),
'download',
isImg ? 'img' : i?.ext,
`${i?.id}.${i?.ext}`,
);
await fs.removeFile(file);
return file;
});
Promise.all(rows).then(async () => {
await handleRefresh();
message.success('All files selected are cleared!');
});
};
const handleRefresh = async () => {
await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON, dir: 'download' });
rowReset();
const data = await refreshJson();
opInit(data);
};
const handleCancel = () => {
setVisible(false);
opInfo.resetRecord();
};
return (
<div>
<div className="chat-table-btns">
<div>
{selectedItems.length > 0 && (
<>
<Popconfirm
overlayStyle={{ width: 250 }}
title="Files 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_DOWNLOAD_JSON} />
<Table
rowKey="id"
columns={columns}
scroll={{ x: 800 }}
dataSource={opData}
rowSelection={rowSelection}
pagination={TABLE_PAGINATION}
/>
<Modal
open={isVisible}
title={<div>{opInfo?.opRecord?.name || ''}</div>}
onCancel={handleCancel}
footer={false}
destroyOnClose
>
<img style={{ maxWidth: '100%' }} src={source} />
</Modal>
</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

@@ -1,4 +1,4 @@
import { Switch, Tag, Tooltip } from 'antd'; import { Table, Switch, Tag } from 'antd';
import { genCmd } from '@/utils'; import { genCmd } from '@/utils';
@@ -35,13 +35,12 @@ export const syncColumns = () => [
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} /> <Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
), ),
}, },
Table.EXPAND_COLUMN,
{ {
title: 'Prompt', title: 'Prompt',
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>,
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
),
}, },
]; ];

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,12 +1,13 @@
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 useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { fmtDate, chatRoot } from '@/utils'; import { fmtDate, chatRoot } from '@/utils';
import { syncColumns } from './config'; import { syncColumns } from './config';
import './index.scss'; import './index.scss';
@@ -14,7 +15,7 @@ import './index.scss';
const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv'; const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv';
export default function SyncPrompts() { export default function SyncPrompts() {
const { rowSelection, selectedRowIDs } = useTable(); const { rowSelection, selectedRowIDs } = useTableRowSelection();
const [jsonPath, setJsonPath] = useState(''); const [jsonPath, setJsonPath] = useState('');
const { modelJson, modelSet } = useChatModel('sync_prompts'); const { modelJson, modelSet } = useChatModel('sync_prompts');
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath); const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
@@ -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,7 +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>,
}}
/> />
</div> </div>
) );
} }

View File

@@ -1,4 +1,4 @@
import { Switch, Tag, Tooltip } from 'antd'; import { Switch, Tag, Table } from 'antd';
import { genCmd } from '@/utils'; import { genCmd } from '@/utils';
@@ -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>
), ),
}, },
{ {
@@ -37,13 +41,12 @@ export const syncColumns = () => [
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} /> <Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
), ),
}, },
Table.EXPAND_COLUMN,
{ {
title: 'Prompt', title: 'Prompt',
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>,
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
),
}, },
]; ];

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 useTable, { 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';
@@ -19,7 +20,7 @@ export default function SyncRecord() {
const [jsonPath, setJsonPath] = useState(''); const [jsonPath, setJsonPath] = useState('');
const state = location?.state; const state = location?.state;
const { rowSelection, selectedRowIDs } = useTable(); const { rowSelection, selectedRowIDs } = useTableRowSelection();
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath); const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
const { opData, opInit, opReplace, opReplaceItems, opSafeKey } = useData([]); const { opData, opInit, opReplace, opReplaceItems, opSafeKey } = useData([]);
const { columns, ...opInfo } = useColumns(syncColumns()); const { columns, ...opInfo } = useColumns(syncColumns());
@@ -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,7 +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>,
}}
/> />
</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

@@ -1,4 +1,4 @@
import { Tag, Switch, Tooltip, Space, Popconfirm } from 'antd'; import { Tag, Switch, Space, Popconfirm, Table } from 'antd';
export const modelColumns = () => [ export const modelColumns = () => [
{ {
@@ -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>
), ),
}, },
{ {
@@ -33,14 +37,13 @@ export const modelColumns = () => [
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} /> <Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
), ),
}, },
Table.EXPAND_COLUMN,
{ {
title: 'Prompt', title: 'Prompt',
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>,
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
),
}, },
{ {
title: 'Action', title: 'Action',
@@ -60,5 +63,5 @@ export const modelColumns = () => [
</Popconfirm> </Popconfirm>
</Space> </Space>
), ),
} },
]; ];

View File

@@ -1,18 +1,19 @@
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 useTable, { TABLE_PAGINATION } from '@/hooks/useTable'; import FilePath from '@/components/FilePath';
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
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 } = useTable(); const { rowSelection, selectedRowIDs } = useTableRowSelection();
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const [jsonPath, setJsonPath] = useState(''); const [jsonPath, setJsonPath] = useState('');
const { modelJson, modelSet } = useChatModel('user_custom'); const { modelJson, modelSet } = useChatModel('user_custom');
@@ -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,6 +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>,
}}
/> />
<Modal <Modal
open={isVisible} open={isVisible}
@@ -135,5 +145,5 @@ export default function LanguageModel() {
<UserCustomForm record={opInfo?.opRecord} ref={formRef} /> <UserCustomForm record={opInfo?.opRecord} ref={formRef} />
</Modal> </Modal>
</div> </div>
) );
} }

71
src/view/notes/config.tsx vendored Normal file
View File

@@ -0,0 +1,71 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Space, Popconfirm } from 'antd';
import { path, shell } from '@tauri-apps/api';
import { EditRow } from '@/hooks/useColumns';
import useInit from '@/hooks/useInit';
import { fmtDate, chatRoot } from '@/utils';
export const notesColumns = () => [
{
title: 'Name',
dataIndex: 'name',
fixed: 'left',
key: 'name',
width: 240,
render: (_: string, row: any, actions: any) => (
<EditRow rowKey="name" row={row} actions={actions} />
),
},
{
title: 'Path',
dataIndex: 'path',
key: 'path',
width: 200,
render: (_: string, row: any) => <RenderPath row={row} />,
},
{
title: 'Created',
dataIndex: 'created',
key: 'created',
width: 150,
render: fmtDate,
},
{
title: 'Action',
fixed: 'right',
width: 160,
render: (_: any, row: any, actions: any) => {
return (
<Space>
<a onClick={() => actions.setRecord(row, 'preview')}>Preview</a>
<Link to={`/md/${row.id}`} state={row}>
Edit
</Link>
<Popconfirm
title="Are you sure to delete this file?"
onConfirm={() => actions.setRecord(row, 'delete')}
okText="Yes"
cancelText="No"
>
<a>Delete</a>
</Popconfirm>
</Space>
);
},
},
];
const RenderPath = ({ row }: any) => {
const [filePath, setFilePath] = useState('');
useInit(async () => {
setFilePath(await getPath(row));
});
return <a onClick={() => shell.open(filePath)}>{filePath}</a>;
};
export const getPath = async (row: any) => {
return (await path.join(await chatRoot(), 'notes', row.id)) + `.${row.ext}`;
};

126
src/view/notes/index.tsx vendored Normal file
View File

@@ -0,0 +1,126 @@
import { useEffect, useState } from 'react';
import { Table, Modal, Popconfirm, Button, message } from 'antd';
import { invoke, path, fs } from '@tauri-apps/api';
import useJson from '@/hooks/useJson';
import useData from '@/hooks/useData';
import useColumns from '@/hooks/useColumns';
import Markdown from '@/components/Markdown';
import FilePath from '@/components/FilePath';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { chatRoot, CHAT_NOTES_JSON } from '@/utils';
import { notesColumns } from './config';
export default function Notes() {
const [source, setSource] = useState('');
const [isVisible, setVisible] = useState(false);
const { opData, opInit, opReplace, opSafeKey } = useData([]);
const { columns, ...opInfo } = useColumns(notesColumns());
const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' });
const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_NOTES_JSON);
const selectedItems = rowSelection.selectedRowKeys || [];
useEffect(() => {
if (!json || json.length <= 0) return;
opInit(json);
}, [json?.length]);
useEffect(() => {
if (!opInfo.opType) return;
(async () => {
const record = opInfo?.opRecord;
const file = await path.join(await chatRoot(), 'notes', `${record?.id}.${record?.ext}`);
if (opInfo.opType === 'preview') {
const data = await fs.readTextFile(file);
setSource(data);
setVisible(true);
return;
}
if (opInfo.opType === 'delete') {
await fs.removeFile(file);
await handleRefresh();
}
if (opInfo.opType === 'rowedit') {
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
await updateJson(data);
message.success('Name has been changed!');
}
opInfo.resetRecord();
})();
}, [opInfo.opType]);
const handleDelete = async () => {
if (opData?.length === selectedRows.length) {
const notesDir = await path.join(await chatRoot(), 'notes');
await fs.removeDir(notesDir, { recursive: true });
await handleRefresh();
message.success('All files have been cleared!');
return;
}
const rows = selectedRows.map(async (i) => {
const file = await path.join(await chatRoot(), 'notes', `${i?.id}.${i?.ext}`);
await fs.removeFile(file);
return file;
});
Promise.all(rows).then(async () => {
await handleRefresh();
message.success('All files selected are cleared!');
});
};
const handleRefresh = async () => {
await invoke('download_list', { pathname: CHAT_NOTES_JSON, dir: 'notes' });
rowReset();
const data = await refreshJson();
opInit(data);
};
const handleCancel = () => {
setVisible(false);
opInfo.resetRecord();
};
return (
<div>
<div className="chat-table-btns">
<div>
{selectedItems.length > 0 && (
<>
<Popconfirm
overlayStyle={{ width: 250 }}
title="Files 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_NOTES_JSON} />
<Table
rowKey="id"
columns={columns}
scroll={{ x: 800 }}
dataSource={opData}
rowSelection={rowSelection}
pagination={TABLE_PAGINATION}
/>
<Modal
open={isVisible}
title={<div>{opInfo?.opRecord?.name || ''}</div>}
onCancel={handleCancel}
footer={false}
destroyOnClose
width={600}
>
<Markdown children={source} />
</Modal>
</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, CHAT_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_chat_conf'));
setPath(await path.join(await chatRoot(), CHAT_CONF_JSON));
});
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 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={CHAT_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,
}, },