mirror of
https://github.com/FranP-code/ChatGPT.git
synced 2025-10-13 00:13:25 +00:00
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0ff062b21 | ||
|
|
ad722b09cf | ||
|
|
d78f23d7e2 | ||
|
|
0c68599d35 | ||
|
|
ca171b7c1b | ||
|
|
7aa70c83de | ||
|
|
585582244d | ||
|
|
1c3b3c1f22 | ||
|
|
db7a4f0b01 | ||
|
|
655dda8efb | ||
|
|
f09204ff5a | ||
|
|
817cd6f87c | ||
|
|
c55ac1df3b | ||
|
|
9ba3357a58 | ||
|
|
221287bbd5 | ||
|
|
3fea94f669 | ||
|
|
9a5c008a25 | ||
|
|
11e20e7f8c | ||
|
|
f0e46639d0 | ||
|
|
7d602e01a9 | ||
|
|
e6052152ea | ||
|
|
ba438b0640 | ||
|
|
4a7ee4dcf5 | ||
|
|
f1e528d3a7 | ||
|
|
1f573102d3 | ||
|
|
d5df706b47 | ||
|
|
6cf7da5557 | ||
|
|
84a29d7cda | ||
|
|
f1ec7c6495 | ||
|
|
6a7eabf5cb | ||
|
|
d84c11319d | ||
|
|
bc39dcdd72 | ||
|
|
1ba356a91f | ||
|
|
c5ba0f783a | ||
|
|
c508633262 | ||
|
|
1d7bb3e051 | ||
|
|
b875727753 | ||
|
|
1e5ec6028d | ||
|
|
5b6a69444e | ||
|
|
321007bb87 | ||
|
|
5f1c33d750 | ||
|
|
1af173cb24 | ||
|
|
8a3ccb6231 | ||
|
|
e868080a69 | ||
|
|
a7d12bafc0 | ||
|
|
f38d683f4e | ||
|
|
240c88220d | ||
|
|
f822a56993 | ||
|
|
fb88ba8a1f | ||
|
|
3ffd832640 | ||
|
|
59c5a9932d | ||
|
|
7c0003f823 | ||
|
|
a798a8a784 | ||
|
|
18c6b1e4ad | ||
|
|
4f03487a53 | ||
|
|
ef3820fad8 | ||
|
|
f0c635bd3b | ||
|
|
6d950c09e6 | ||
|
|
26bd845a72 | ||
|
|
96f7e32137 | ||
|
|
3c848b4ded | ||
|
|
f1a807ed46 | ||
|
|
ae2c56805c | ||
|
|
a2fcfa3b89 | ||
|
|
0eb6c559c6 | ||
|
|
bd5f34b7f9 | ||
|
|
042155ffdd | ||
|
|
08ef0a2437 | ||
|
|
e473268df1 | ||
|
|
e24fd6a33f | ||
|
|
eccee44866 | ||
|
|
7446cfc186 | ||
|
|
2dfb9bac2a | ||
|
|
2764219867 | ||
|
|
e68ab20420 | ||
|
|
c99f4b7633 | ||
|
|
335d3e33ba |
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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.'
|
||||||
|
|||||||
70
.github/ISSUE_TEMPLATE/build_error_report.yml
vendored
70
.github/ISSUE_TEMPLATE/build_error_report.yml
vendored
@@ -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.'
|
||||||
|
|||||||
34
.github/ISSUE_TEMPLATE/docmentation_issue.yml
vendored
34
.github/ISSUE_TEMPLATE/docmentation_issue.yml
vendored
@@ -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.
|
||||||
|
|||||||
64
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
64
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -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.
|
||||||
|
|||||||
64
.github/ISSUE_TEMPLATE/security.yml
vendored
64
.github/ISSUE_TEMPLATE/security.yml
vendored
@@ -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.
|
||||||
|
|||||||
20
.github/workflows/release.yml
vendored
20
.github/workflows/release.yml
vendored
@@ -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
6
.husky/pre-commit
Executable 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
45
.prettierignore
Normal 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
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
9
Cargo.toml
Normal file
9
Cargo.toml
Normal 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"
|
||||||
@@ -9,7 +9,9 @@
|
|||||||

|

|
||||||
[](https://github.com/lencx/ChatGPT/releases)
|
[](https://github.com/lencx/ChatGPT/releases)
|
||||||
[](https://discord.gg/aPhCRf4zZr)
|
[](https://discord.gg/aPhCRf4zZr)
|
||||||
[](https://twitter.com/lencx_)
|
[](https://twitter.com/lencx_)
|
||||||
|
|
||||||
|
<!-- [](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`,来一键同步所有,如果你不想让某些提示出现在你的斜杠命令,你可以禁用它们。
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
<!-- 数据导入完成后,可以重新启动应用来使配置生效(`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) 的插件获得,并做了一些本地化修改
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -9,7 +9,9 @@
|
|||||||

|

|
||||||
[](https://github.com/lencx/ChatGPT/releases)
|
[](https://github.com/lencx/ChatGPT/releases)
|
||||||
[](https://discord.gg/aPhCRf4zZr)
|
[](https://discord.gg/aPhCRf4zZr)
|
||||||
[](https://twitter.com/lencx_)
|
[](https://twitter.com/lencx_)
|
||||||
|
|
||||||
|
<!-- [](https://twitter.com/lencx_) -->
|
||||||
|
|
||||||
<!-- [](./README-ZH.md) -->
|
<!-- [](./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.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
<!-- 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 configuration,contains three parts:
|
- `chat.model.json` - prompts configuration,contains 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.
|
||||||
|
|||||||
198
UPDATE_LOG.md
198
UPDATE_LOG.md
@@ -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
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 |
19
package.json
19
package.json
@@ -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
14
rustfmt.toml
Normal 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
6
scripts/download.js
vendored
@@ -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);
|
||||||
|
|||||||
@@ -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"
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
294
src-tauri/src/app/gpt.rs
Normal 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
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
280
src-tauri/src/assets/cmd.js
vendored
280
src-tauri/src/assets/cmd.js
vendored
@@ -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);
|
|
||||||
}
|
|
||||||
103
src-tauri/src/assets/core.js
vendored
103
src-tauri/src/assets/core.js
vendored
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
296
src-tauri/src/scripts/cmd.js
vendored
Normal 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
180
src-tauri/src/scripts/core.js
vendored
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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];
|
|
||||||
}
|
|
||||||
36
src-tauri/src/scripts/markdown.export.js
vendored
Normal file
36
src-tauri/src/scripts/markdown.export.js
vendored
Normal 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;
|
||||||
|
}({}));
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
164
src-tauri/src/vendors/turndown-plugin-gfm.js
vendored
Normal file
164
src-tauri/src/vendors/turndown-plugin-gfm.js
vendored
Normal 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
1
src-tauri/src/vendors/turndown.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
41
src/components/FilePath/index.tsx
vendored
Normal 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
48
src/components/Markdown/Editor.tsx
vendored
Normal 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
33
src/components/Markdown/index.scss
vendored
Normal 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
52
src/components/Markdown/index.tsx
vendored
Normal 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
98
src/components/SwitchOrigin/index.tsx
vendored
Normal 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;
|
||||||
12
src/components/Tags/index.tsx
vendored
12
src/components/Tags/index.tsx
vendored
@@ -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;
|
||||||
|
|||||||
14
src/hooks/useChatModel.ts
vendored
14
src/hooks/useChatModel.ts
vendored
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
44
src/hooks/useColumns.ts
vendored
44
src/hooks/useColumns.ts
vendored
@@ -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
83
src/hooks/useColumns.tsx
vendored
Normal 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
27
src/hooks/useData.ts
vendored
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
4
src/hooks/useInit.ts
vendored
4
src/hooks/useInit.ts
vendored
@@ -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
23
src/hooks/useJson.ts
vendored
Normal 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 };
|
||||||
|
}
|
||||||
44
src/hooks/useTable.tsx
vendored
44
src/hooks/useTable.tsx
vendored
@@ -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
25
src/icons/SplitIcon.tsx
vendored
Normal 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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
src/layout/index.scss
vendored
6
src/layout/index.scss
vendored
@@ -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
144
src/layout/index.tsx
vendored
@@ -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
43
src/main.scss
vendored
@@ -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
4
src/main.tsx
vendored
@@ -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
77
src/routes.tsx
vendored
@@ -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
66
src/utils.ts
vendored
@@ -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
182
src/view/General.tsx
vendored
@@ -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
28
src/view/about/index.scss
vendored
Normal 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
88
src/view/about/index.tsx
vendored
Normal 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
63
src/view/awesome/Form.tsx
vendored
Normal 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
74
src/view/awesome/config.tsx
vendored
Normal 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
197
src/view/awesome/index.tsx
vendored
Normal 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
73
src/view/dashboard/index.scss
vendored
Normal 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
108
src/view/dashboard/index.tsx
vendored
Normal 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
82
src/view/download/config.tsx
vendored
Normal 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
145
src/view/download/index.tsx
vendored
Normal 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
16
src/view/markdown/index.scss
vendored
Normal 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
58
src/view/markdown/index.tsx
vendored
Normal 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]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/view/model/SyncCustom/Form.tsx
vendored
58
src/view/model/SyncCustom/Form.tsx
vendored
@@ -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);
|
||||||
|
|||||||
26
src/view/model/SyncCustom/config.tsx
vendored
26
src/view/model/SyncCustom/config.tsx
vendored
@@ -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}`;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
50
src/view/model/SyncCustom/index.tsx
vendored
50
src/view/model/SyncCustom/index.tsx
vendored
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/view/model/SyncPrompts/config.tsx
vendored
7
src/view/model/SyncPrompts/config.tsx
vendored
@@ -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>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
3
src/view/model/SyncPrompts/index.scss
vendored
3
src/view/model/SyncPrompts/index.scss
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/view/model/SyncPrompts/index.tsx
vendored
30
src/view/model/SyncPrompts/index.tsx
vendored
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/view/model/SyncRecord/config.tsx
vendored
13
src/view/model/SyncRecord/config.tsx
vendored
@@ -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>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
36
src/view/model/SyncRecord/index.tsx
vendored
36
src/view/model/SyncRecord/index.tsx
vendored
@@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/view/model/UserCustom/Form.tsx
vendored
29
src/view/model/UserCustom/Form.tsx
vendored
@@ -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);
|
||||||
|
|||||||
17
src/view/model/UserCustom/config.tsx
vendored
17
src/view/model/UserCustom/config.tsx
vendored
@@ -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>
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
98
src/view/model/UserCustom/index.tsx
vendored
98
src/view/model/UserCustom/index.tsx
vendored
@@ -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
71
src/view/notes/config.tsx
vendored
Normal 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
126
src/view/notes/index.tsx
vendored
Normal 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
95
src/view/settings/General.tsx
vendored
Normal 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
49
src/view/settings/MainWindow.tsx
vendored
Normal 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
36
src/view/settings/TrayWindow.tsx
vendored
Normal 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
117
src/view/settings/index.tsx
vendored
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user