Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e2d395156 | ||
|
|
990aa31437 | ||
|
|
a73d203983 | ||
|
|
2a9fba7d27 | ||
|
|
e4be2bc2f3 | ||
|
|
389e00a5e0 | ||
|
|
2be560e69a | ||
|
|
6abe7c783e | ||
|
|
8319eae519 | ||
|
|
921d670f53 | ||
|
|
39a8d8d297 | ||
|
|
2d826c90a0 | ||
|
|
d513a50e27 | ||
|
|
878bb6c265 | ||
|
|
69f1968e88 | ||
|
|
3a0ee7d4d6 | ||
|
|
5dd671c98e | ||
|
|
75a7b9c78d | ||
|
|
47a3bace5b | ||
|
|
8966ebbd03 | ||
|
|
3fe04a244a | ||
|
|
c54aec88c0 | ||
|
|
02fb4dd3b7 | ||
|
|
028ef8bae8 | ||
|
|
e86bf42cc1 | ||
|
|
09b8643d99 | ||
|
|
c07fd1e0b8 | ||
|
|
1b71bf8f26 | ||
|
|
4366b8ee8a | ||
|
|
7505311a2c | ||
|
|
680100801f | ||
|
|
ee0836cb07 | ||
|
|
91cebe82db | ||
|
|
7fea0aa395 | ||
|
|
d554dcda80 | ||
|
|
2393d9d555 | ||
|
|
24a7d60257 | ||
|
|
b84f45f932 | ||
|
|
e524f12b6a | ||
|
|
4df09113b5 | ||
|
|
1e7c0fe02a | ||
|
|
47c9072f40 | ||
|
|
3318bfb23f | ||
|
|
20105d54be | ||
|
|
305e784145 | ||
|
|
647a89fdf8 | ||
|
|
7da70733a3 | ||
|
|
0649723c3c | ||
|
|
373097a54b | ||
|
|
5a9a03aeab | ||
|
|
248d115c8a | ||
|
|
8f1ef2f306 | ||
|
|
fd6edb7225 | ||
|
|
38b3f77eb4 | ||
|
|
cfa88473c0 | ||
|
|
d1f10672fa | ||
|
|
4d698eabba | ||
|
|
d0d2869dff | ||
|
|
70881f95da | ||
|
|
60f6628ec0 | ||
|
|
60abf6e487 | ||
|
|
216ef3ff1a | ||
|
|
7b12d3ebfe | ||
|
|
396dc0b762 | ||
|
|
cd7d5fab63 | ||
|
|
8363ff234d | ||
|
|
430d6e4af2 | ||
|
|
918d53c9b9 | ||
|
|
a88c3603cc | ||
|
|
785be23528 | ||
|
|
bb44cdd5b7 | ||
|
|
5a835c114e | ||
|
|
3ea8f44452 | ||
|
|
04a579ff19 | ||
|
|
f08cd978cb | ||
|
|
7cf20cb571 |
5
.gitattributes
vendored
@@ -1 +1,4 @@
|
|||||||
*.js linguist-vendored
|
*.js linguist-vendored
|
||||||
|
*.tsx linguist-vendored
|
||||||
|
*.scss linguist-vendored
|
||||||
|
src/**/*.ts linguist-vendored
|
||||||
2
.github/workflows/release.yml
vendored
@@ -75,7 +75,7 @@ jobs:
|
|||||||
${{ runner.os }}-yarn-
|
${{ runner.os }}-yarn-
|
||||||
|
|
||||||
- name: Install app dependencies and build it
|
- name: Install app dependencies and build it
|
||||||
run: yarn
|
run: yarn && yarn build:fe
|
||||||
|
|
||||||
- uses: tauri-apps/tauri-action@v0.3
|
- uses: tauri-apps/tauri-action@v0.3
|
||||||
env:
|
env:
|
||||||
|
|||||||
29
.gitignore
vendored
@@ -1,9 +1,32 @@
|
|||||||
.DS_Store
|
|
||||||
*.lock
|
|
||||||
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
node_modules/
|
node_modules/
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
*.lock
|
||||||
|
|
||||||
|
# rust
|
||||||
target/
|
target/
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
|
||||||
|
# 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?
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
- [ChatGPT Export and Share](https://github.com/liady/ChatGPT-pdf) - A Chrome extension for downloading your ChatGPT history to PNG, PDF or creating a sharable link
|
- [ChatGPT Export and Share](https://github.com/liady/ChatGPT-pdf) - A Chrome extension for downloading your ChatGPT history to PNG, PDF or creating a sharable link
|
||||||
- [ChatGPT for Google](https://github.com/wong2/chat-gpt-google-extension) - A browser extension to display ChatGPT response alongside Google Search results
|
- [ChatGPT for Google](https://github.com/wong2/chat-gpt-google-extension) - A browser extension to display ChatGPT response alongside Google Search results
|
||||||
- [ChatGPT Extension](https://github.com/kazuki-sf/ChatGPT_Extension) - ChatGPT Extension is a really simple Chrome Extension (manifest v3) that you can access OpenAI's ChatGPT from anywhere on the web.
|
- [ChatGPT Extension](https://github.com/kazuki-sf/ChatGPT_Extension) - ChatGPT Extension is a really simple Chrome Extension (manifest v3) that you can access OpenAI's ChatGPT from anywhere on the web.
|
||||||
|
- [ChatGPT-Google](https://github.com/ZohaibAhmed/ChatGPT-Google) - Chrome Extension that Integrates ChatGPT (Unofficial) into Google Search
|
||||||
|
|
||||||
`VSCode`
|
`VSCode`
|
||||||
|
|
||||||
@@ -18,3 +19,8 @@
|
|||||||
`Bot`
|
`Bot`
|
||||||
|
|
||||||
- [ChatGPT Telegram Bot](https://github.com/altryne/chatGPT-telegram-bot) - This is a very early attempt at having chatGPT work within a telegram bot
|
- [ChatGPT Telegram Bot](https://github.com/altryne/chatGPT-telegram-bot) - This is a very early attempt at having chatGPT work within a telegram bot
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- [commitgpt](https://github.com/RomanHotsiy/commitgpt) - Automatically generate commit messages using ChatGPT
|
||||||
|
- [ShareGPT](https://sharegpt.com/) - ShareGPT: Share your wildest ChatGPT conversations with one click.
|
||||||
|
|||||||
214
LICENSE
@@ -1,21 +1,201 @@
|
|||||||
MIT License
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
Copyright (c) 2022 lencx
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
1. Definitions.
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
copies or substantial portions of the Software.
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
the copyright owner that is granting the License.
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
other entities that control, are controlled by, or are under common
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
control with that entity. For the purposes of this definition,
|
||||||
SOFTWARE.
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
229
README-ZH_CN.md
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img width="180" src="./public/logo.png" alt="ChatGPT">
|
||||||
|
<h1 align="center">ChatGPT</h1>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
> ChatGPT 桌面应用
|
||||||
|
|
||||||
|
[](./README.md)
|
||||||
|
[](./README-ZH_CN.md)
|
||||||
|
|
||||||
|
[](https://github.com/lencx/ChatGPT/releases)
|
||||||
|
[](https://discord.gg/aPhCRf4zZr)
|
||||||
|
[](https://twitter.com/lencx_)
|
||||||
|
|
||||||
|
[Awesome ChatGPT](./AWESOME.md)
|
||||||
|
|
||||||
|
## 📦 下载
|
||||||
|
|
||||||
|
[📝 更新日志](./UPDATE_LOG.md)
|
||||||
|
|
||||||
|
<!-- download start -->
|
||||||
|
|
||||||
|
**最新版:**
|
||||||
|
|
||||||
|
- `Mac`: [ChatGPT_0.6.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.6.0/ChatGPT_0.6.0_x64.dmg)
|
||||||
|
- `Linux`: [chat-gpt_0.6.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.6.0/chat-gpt_0.6.0_amd64.deb)
|
||||||
|
- `Windows`: [ChatGPT_0.6.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.6.0/ChatGPT_0.6.0_x64_en-US.msi)
|
||||||
|
|
||||||
|
[其他版本...](https://github.com/lencx/ChatGPT/releases)
|
||||||
|
|
||||||
|
<!-- download end -->
|
||||||
|
|
||||||
|
### brew 安装
|
||||||
|
|
||||||
|
Easily install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
|
||||||
|
|
||||||
|
```sh
|
||||||
|
brew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git
|
||||||
|
brew install --cask chatgpt --no-quarantine
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, if you keep a _[Brewfile](https://github.com/Homebrew/homebrew-bundle#usage)_, you can add something like this:
|
||||||
|
|
||||||
|
```rb
|
||||||
|
repo = "lencx/chatgpt"
|
||||||
|
tap repo, "https://github.com/#{repo}.git"
|
||||||
|
cask "popcorn-time", args: { "no-quarantine": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📢 公告
|
||||||
|
|
||||||
|
这是一个令人兴奋的重大更新。像 `Telegram 机器人指令` 那样工作,帮助你快速填充自定模型,来让 ChatGPT 按照你想要的方式去工作。这个项目倾注了我大量业余时间,如果它对你有所帮助,宣传转发,或者 star 都是对我的巨大鼓励。我希望我可以持续更新下去,加入更多有趣的功能。
|
||||||
|
|
||||||
|
### 如何使用指令?
|
||||||
|
|
||||||
|
你可以从 [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 来寻找有趣的功能来导入到应用。
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。
|
||||||
|
|
||||||
|
在 ChatGPT 文本输入区域,键入 `/` 开头的字符,则会弹出指令提示,按下空格键,它会默认将命令关联的文本填充到输入区域(注意:如果包含多个指令提示,它只会选择第一个作为填充,你可以持续输入,直到第一个提示命令为你想要时,再按下空格键。或者使用鼠标来点击多条指令中的某一个)。填充完成后,你只需要按下回车键即可。斜杠命令下,使用 TAB 键修改 `{q}` 标签内容(仅支持单个修改 [#54](https://github.com/lencx/ChatGPT/issues/54))。
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## ✨ 功能概览
|
||||||
|
|
||||||
|
- 跨平台: `macOS` `Linux` `Windows`
|
||||||
|
- 导出 ChatGPT 聊天记录 (支持 PNG, PDF 和生成分享链接)
|
||||||
|
- 应用自动升级通知
|
||||||
|
- 丰富的快捷键
|
||||||
|
- 系统托盘悬浮窗
|
||||||
|
- 应用菜单功能强大
|
||||||
|
- 支持斜杠命令及其配置(可手动配置或从文件同步)
|
||||||
|
|
||||||
|
### 菜单项
|
||||||
|
|
||||||
|
- **Preferences (喜好)**
|
||||||
|
- `Theme` - `Light`, `Dark` (仅支持 macOS 和 Windows)
|
||||||
|
- `Stay On Top`: 窗口置顶
|
||||||
|
- `Titlebar`: 是否显示 `Titlebar`,仅 macOS 支持
|
||||||
|
- `Inject Script`: 用于修改网站的用户自定义脚本
|
||||||
|
- `Hide Dock Icon` ([#35](https://github.com/lencx/ChatGPT/issues/35)): 隐藏 Dock 中的应用图标 (仅 macOS 支持)
|
||||||
|
- 系统图盘右键单击打开菜单,然后在菜单项中点击 `Show Dock Icon` 可以重新将应用图标显示在 Dock(`SystemTrayMenu -> Show Dock Icon`)
|
||||||
|
- `Control Center`: ChatGPT 应用的控制中心,它将为应用提供无限的可能
|
||||||
|
- 设置 `Theme`,`Stay On Top`,`Titlebar` 等
|
||||||
|
- `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): 自定义 `user agent` 防止网站安全检测,默认值为空
|
||||||
|
- `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): 切换网站源地址,默认为 `https://chat.openai.com`。需要注意的是镜像网站的 UI 需要和原网站一致,否则可能会导致某些功能不工作
|
||||||
|
- `Go to Config`: 打开 ChatGPT 配置目录 (`path: ~/.chatgpt/*`)
|
||||||
|
- `Clear Config`: 清除 ChatGPT 配置数据 (`path: ~/.chatgpt/*`), 这是危险操作,请提前备份数据
|
||||||
|
- `Restart ChatGPT`: 重启应用。如果注入脚本编辑完成,或者应用可卡死可以通过此菜单重新启动应用
|
||||||
|
- `Awesome ChatGPT`: 一个很棒的 ChatGPT 推荐列表
|
||||||
|
- **Edit** - `Undo`, `Redo`, `Cut`, `Copy`, `SelectAll`, ...
|
||||||
|
- **View** - `Go Back`, `Go Forward`, `Scroll to Top of Screen`, `Scroll to Bottom of Screen`, `Refresh the Screen`, ...
|
||||||
|
- **Help**
|
||||||
|
- `Update Log`: ChatGPT 应用更新日志
|
||||||
|
- `Report Bug`: 报告 BUG 或反馈建议
|
||||||
|
- `Toggle Developer Tools`: 网站调试工具,调试页面或脚本可能需要
|
||||||
|
|
||||||
|
## 应用配置
|
||||||
|
|
||||||
|
| 平台 | 路径 |
|
||||||
|
| ------- | ------------------------- |
|
||||||
|
| Linux | `/home/lencx/.chatgpt` |
|
||||||
|
| macOS | `/Users/lencx/.chatgpt` |
|
||||||
|
| Windows | `C:\Users\lencx\.chatgpt` |
|
||||||
|
|
||||||
|
- `[.chatgpt]` - 应用配置根路径
|
||||||
|
- `chat.conf.json` - 应用喜好配置
|
||||||
|
- `chat.model.json` - ChatGPT 输入提示,通过斜杠命令来快速完成输入,主要包含三部分:
|
||||||
|
- `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_custom` - 同步自定义的 json 或 csv 文件数据,支持本地和远程 (**Control Conter -> Language Model -> Sync Custom**)
|
||||||
|
- `chat.model.cmd.json` - 过滤(是否启用)和排序处理后的斜杠命令数据
|
||||||
|
- `[cache_model]` - 缓存同步或录入的数据
|
||||||
|
- `chatgpt_prompts.json` - 缓存 `sync_prompts` 数据
|
||||||
|
- `user_custom.json` - 缓存 `user_custom` 数据
|
||||||
|
- `ae6cf32a6f8541b499d6bfe549dbfca3.json` - 随机生成的文件名,缓存 `sync_custom` 数据
|
||||||
|
- `4f695d3cfbf8491e9b1f3fab6d85715c.json` - 随机生成的文件名,缓存 `sync_custom` 数据
|
||||||
|
- `bd1b96f15a1644f7bd647cc53073ff8f.json` - 随机生成的文件名,缓存 `sync_custom` 数据
|
||||||
|
|
||||||
|
### Sync Custom
|
||||||
|
|
||||||
|
目前同步自定文件仅支持 json 和 csv,且需要满足以下格式,否则会导致应用异常:
|
||||||
|
|
||||||
|
> JSON 格式
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"cmd": "a",
|
||||||
|
"act": "aa",
|
||||||
|
"prompt": "aaa aaa aaa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": "b",
|
||||||
|
"act": "bb",
|
||||||
|
"prompt": "bbb bbb bbb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
> CSV 格式
|
||||||
|
|
||||||
|
```csv
|
||||||
|
"cmd","act","prompt"
|
||||||
|
"a","aa","aaa aaa aaa"
|
||||||
|
"b","bb","bbb bbb bbb"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 👀 预览
|
||||||
|
|
||||||
|
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/control-center.png" alt="control center">
|
||||||
|
<img width="320" src="./assets/export.png" alt="export"> <img width="320" src="./assets/tray.png" alt="tray">
|
||||||
|
<img width="320" src="./assets/tray-login.png" alt="tray login"> <img width="320" src="./assets/auto-update.png" alt="auto update">
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<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: 60px !important;width: 217px !important;" ></a>
|
||||||
|
|
||||||
|
## ❓ 常见问题
|
||||||
|
|
||||||
|
### 不能打开 ChatGPT
|
||||||
|
|
||||||
|
如果升级应用后无法打开,请尝试清除配置,它位于此目录 `~/.chatgpt/*`。
|
||||||
|
|
||||||
|
### 主窗口已经登录,但是系统托盘窗口显示未登录
|
||||||
|
|
||||||
|
可通过菜单项里的 `Restart ChatGPT` 重启应用来修复这个问题(`Menu -> Preferences -> Restart ChatGPT`)。
|
||||||
|
|
||||||
|
### 它是否安全?
|
||||||
|
|
||||||
|
它是安全的,仅仅只是对 [OpenAI ChatGPT](https://chat.openai.com) 网站的包装,注入了一些额外功能(均在本地,未发起网络请求),如果存疑,可以检查源代码。
|
||||||
|
|
||||||
|
### Developer cannot be verified?
|
||||||
|
|
||||||
|
Mac 上无法安装,提示开发者未验证,具体可以查看下面给出的解决方案(它是开源的,很安全)。
|
||||||
|
|
||||||
|
- [Open a Mac app from an unidentified developer](https://support.apple.com/en-sg/guide/mac-help/mh40616/mac)
|
||||||
|
|
||||||
|
### 我想自己构建它?
|
||||||
|
|
||||||
|
#### 预安装
|
||||||
|
|
||||||
|
- [Rust](https://www.rust-lang.org/)
|
||||||
|
- [VS Code](https://code.visualstudio.com/)
|
||||||
|
- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
|
- [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode)
|
||||||
|
|
||||||
|
#### 开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# step1: 克隆仓库
|
||||||
|
git clone https://github.com/lencx/ChatGPT.git
|
||||||
|
|
||||||
|
# step2: 进入目录
|
||||||
|
cd ChatGPT
|
||||||
|
|
||||||
|
# step3: 安装依赖
|
||||||
|
yarn
|
||||||
|
|
||||||
|
# step4: 开发启动
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# step5: 构建应用
|
||||||
|
# 构建后的安装包位置: src-tauri/target/release/bundle
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
## ❤️ 感谢
|
||||||
|
|
||||||
|
- 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改
|
||||||
|
- 感谢 [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) 项目为这个应用自定义指令功能所带来的启发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[](https://star-history.com/#lencx/chatgpt&Date)
|
||||||
|
|
||||||
|
## 中国用户
|
||||||
|
|
||||||
|
国内用户如果遇到使用问题或者想交流 ChatGPT 技巧,可以关注公众号“浮之静”,发送 “chat” 进群参与讨论。公众号会更新[《Tauri 系列》](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIzNjE2NTI3NQ==&action=getalbum&album_id=2593843659863752704)文章,技术思考等等,如果对 tauri 开发应用感兴趣可以关注公众号后回复 “tauri” 进技术开发群(想私聊的也可以关注公众号,来添加微信)。开源不易,如果这个项目对你有帮助可以分享给更多人,或者微信扫码打赏。
|
||||||
|
|
||||||
|
<img width="180" src="https://user-images.githubusercontent.com/16164244/207228300-ea5c4688-c916-4c55-a8c3-7f862888f351.png"> <img width="200" src="https://user-images.githubusercontent.com/16164244/207228025-117b5f77-c5d2-48c2-a070-774b7a1596f2.png">
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache License
|
||||||
167
README.md
@@ -1,13 +1,19 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img width="180" src="./logo.png" alt="ChatGPT">
|
<img width="180" src="./public/logo.png" alt="ChatGPT">
|
||||||
<h1 align="center">ChatGPT</h1>
|
<h1 align="center">ChatGPT</h1>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> ChatGPT Desktop Application
|
> ChatGPT Desktop Application
|
||||||
|
|
||||||
|
[](./README.md)
|
||||||
|
[](./README-ZH_CN.md)
|
||||||
|
|
||||||
[](https://github.com/lencx/ChatGPT/releases)
|
[](https://github.com/lencx/ChatGPT/releases)
|
||||||
|
[](https://discord.gg/aPhCRf4zZr)
|
||||||
[](https://twitter.com/lencx_)
|
[](https://twitter.com/lencx_)
|
||||||
|
|
||||||
|
<!-- [](./README-ZH.md) -->
|
||||||
|
|
||||||
[Awesome ChatGPT](./AWESOME.md)
|
[Awesome ChatGPT](./AWESOME.md)
|
||||||
|
|
||||||
## 📦 Downloads
|
## 📦 Downloads
|
||||||
@@ -18,9 +24,9 @@
|
|||||||
|
|
||||||
**Latest:**
|
**Latest:**
|
||||||
|
|
||||||
- `Mac`: [ChatGPT_0.2.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.2.0/ChatGPT_0.2.0_x64.dmg)
|
- `Mac`: [ChatGPT_0.6.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.6.0/ChatGPT_0.6.0_x64.dmg)
|
||||||
- `Linux`: [chat-gpt_0.2.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.2.0/chat-gpt_0.2.0_amd64.deb)
|
- `Linux`: [chat-gpt_0.6.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.6.0/chat-gpt_0.6.0_amd64.deb)
|
||||||
- `Windows`: [ChatGPT_0.2.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.2.0/ChatGPT_0.2.0_x64_en-US.msi)
|
- `Windows`: [ChatGPT_0.6.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.6.0/ChatGPT_0.6.0_x64_en-US.msi)
|
||||||
|
|
||||||
[Other version...](https://github.com/lencx/ChatGPT/releases)
|
[Other version...](https://github.com/lencx/ChatGPT/releases)
|
||||||
|
|
||||||
@@ -30,59 +36,133 @@
|
|||||||
|
|
||||||
Easily install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
|
Easily install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
|
||||||
|
|
||||||
~~~ sh
|
```sh
|
||||||
brew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git
|
brew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git
|
||||||
brew install --cask chatgpt --no-quarantine
|
brew install --cask chatgpt --no-quarantine
|
||||||
~~~
|
```
|
||||||
|
|
||||||
Also, if you keep a _[Brewfile](https://github.com/Homebrew/homebrew-bundle#usage)_, you can add something like this:
|
Also, if you keep a _[Brewfile](https://github.com/Homebrew/homebrew-bundle#usage)_, you can add something like this:
|
||||||
|
|
||||||
~~~ rb
|
```rb
|
||||||
repo = "lencx/chatgpt"
|
repo = "lencx/chatgpt"
|
||||||
tap repo, "https://github.com/#{repo}.git"
|
tap repo, "https://github.com/#{repo}.git"
|
||||||
cask "popcorn-time", args: { "no-quarantine": true }
|
cask "popcorn-time", args: { "no-quarantine": true }
|
||||||
~~~
|
```
|
||||||
|
|
||||||
|
## 📢 Announcement
|
||||||
|
|
||||||
|
This is a major and exciting update. It works like a `Telegram bot command` and helps you quickly populate custom models to make chatgpt work the way you want it to. This project has taken a lot of my spare time, so if it helps you, please help spread the word or star it would be a great encouragement to me. I hope I can keep updating it and adding more interesting features.
|
||||||
|
|
||||||
|
### How does it work?
|
||||||
|
|
||||||
|
You can look at [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) to find interesting features to import into the app.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`).
|
||||||
|
|
||||||
|
In the chatgpt text input area, type a character starting with `/` to bring up the command prompt, press the spacebar, and it will fill the input area with the text associated with the command by default (note: if it contains multiple command prompts, it will only select the first one as the fill, you can keep typing until the first prompted command is the one you want, then press the spacebar. Or use the mouse to click on one of the multiple commands). When the fill is complete, you simply press the Enter key. Under the slash command, use the tab key to modify the contents of the `{q}` tag (only single changes are supported [#54](https://github.com/lencx/ChatGPT/issues/54)).
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## ✨ 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 Share Link)
|
||||||
- always on top (whether the window should always be on top of other windows)
|
- Automatic application upgrade notification
|
||||||
- inject script
|
- Common shortcut keys
|
||||||
- auto updater
|
- System tray hover window
|
||||||
- app menu
|
- Powerful menu items
|
||||||
- tray window
|
- Support for slash commands and their configuration (can be configured manually or synchronized from a file)
|
||||||
- shortcut
|
|
||||||
|
|
||||||
### Menu
|
### MenuItem
|
||||||
|
|
||||||
- **Preferences**
|
- **Preferences**
|
||||||
- `Theme` - `Light`, `Dark` (Only macOS and Windows are supported).
|
- `Theme` - `Light`, `Dark` (Only macOS and Windows are supported).
|
||||||
- `Always On Top`: Window is always on top of other windows.
|
- `Stay On Top`: The window is stay on top of other windows.
|
||||||
- `Titlebar`: Only supports macOS.
|
- `Titlebar`: Whether to display the titlebar, supported by macOS only.
|
||||||
- `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): Customize `user agent` to prevent security detection interception. Default is empty string.
|
- `Hide Dock Icon` ([#35](https://github.com/lencx/ChatGPT/issues/35)): Hide application icons from the Dock(support macOS only).
|
||||||
- `Inject Script`: User scripts that can modify web pages.
|
- Right-click on the SystemTray to open the menu, then click `Show Dock Icon` in the menu item to re-display the application icon in the Dock (`SystemTrayMenu -> Show Dock Icon`).
|
||||||
- `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): Modify website address, the default is `https://chat.openai.com`. Please ensure that the mirror address is consistent with the UI of the original URL, otherwise the export function will fail.
|
- `Inject Script`: Using scripts to modify pages.
|
||||||
- `Clear Config`: Clear all chatgpt configuration files (`path: ~/.chatgpt/*`), dangerous operation, please backup data.
|
- `Control Center`: The control center of ChatGPT application, it will give unlimited imagination to the application.
|
||||||
- `Restart ChatGPT`: After editing the injection script file, you can restart the application through this menu item to make the script take effect.
|
- `Theme`, `Stay On Top`, `Titlebar`, ...
|
||||||
- `Awesome ChatGPT`: Related resources recommended.
|
- `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): Custom `user agent`, which may be required in some scenarios. The default value is the empty string.
|
||||||
|
- `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): Switch the site source address, the default is `https://chat.openai.com`, please make sure the mirror site UI is the same as the original address. Otherwise, some functions may not be available.
|
||||||
|
- `Go to Config`: Open the configuration file directory (`path: ~/.chatgpt/*`).
|
||||||
|
- `Clear Config`: Clear the configuration file (`path: ~/.chatgpt/*`), dangerous operation, please backup the data in advance.
|
||||||
|
- `Restart ChatGPT`: Restart the application, for example: the program is stuck or the injection script can take effect by restarting the application after editing.
|
||||||
|
- `Awesome ChatGPT`: Recommended Related Resources.
|
||||||
- **Edit** - `Undo`, `Redo`, `Cut`, `Copy`, `SelectAll`, ...
|
- **Edit** - `Undo`, `Redo`, `Cut`, `Copy`, `SelectAll`, ...
|
||||||
- **View** - `Go Back`, `Go Forward`, `Scroll to Top of Screen`, `Scroll to Bottom of Screen`, `Refresh the Screen`, ...
|
- **View** - `Go Back`, `Go Forward`, `Scroll to Top of Screen`, `Scroll to Bottom of Screen`, `Refresh the Screen`, ...
|
||||||
- **Help**
|
- **Help**
|
||||||
- `Update Log`: ChatGPT app changelog.
|
- `Update Log`: ChatGPT changelog.
|
||||||
- `Report Bug`: Defects and Suggestions Feedback.
|
- `Report Bug`: Report a bug or give feedback.
|
||||||
- `Toggle Developer Tools`: Developer tools for debugging web pages.
|
- `Toggle Developer Tools`: Developer debugging tools.
|
||||||
|
|
||||||
|
## Application Configuration
|
||||||
|
|
||||||
|
| Platform | Path |
|
||||||
|
| -------- | ------------------------- |
|
||||||
|
| Linux | `/home/lencx/.chatgpt` |
|
||||||
|
| macOS | `/Users/lencx/.chatgpt` |
|
||||||
|
| Windows | `C:\Users\lencx\.chatgpt` |
|
||||||
|
|
||||||
|
- `[.chatgpt]` - application configuration root folder
|
||||||
|
- `chat.conf.json` - preferences configuration
|
||||||
|
- `chat.model.json` - prompts configuration,contains three parts:
|
||||||
|
- `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_custom` - Synchronize custom json and csv file data, support local and remote (**Control Conter -> Language Model -> Sync Custom**)
|
||||||
|
- `chat.model.cmd.json` - filtered (whether to enable) and sorted slash commands
|
||||||
|
- `[cache_model]` - caching model data
|
||||||
|
- `chatgpt_prompts.json` - Cache `sync_prompts` data
|
||||||
|
- `user_custom.json` - Cache `user_custom` data
|
||||||
|
- `ae6cf32a6f8541b499d6bfe549dbfca3.json` - Randomly generated file names, cache `sync_custom` data
|
||||||
|
- `4f695d3cfbf8491e9b1f3fab6d85715c.json` - Randomly generated file names, cache `sync_custom` data
|
||||||
|
- `bd1b96f15a1644f7bd647cc53073ff8f.json` - Randomly generated file names, cache `sync_custom` data
|
||||||
|
|
||||||
|
### Sync Custom
|
||||||
|
|
||||||
|
Currently, only json and csv are supported for synchronizing custom files, and the following formats need to be met, otherwise the application will be abnormal:
|
||||||
|
|
||||||
|
> JSON format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"cmd": "a",
|
||||||
|
"act": "aa",
|
||||||
|
"prompt": "aaa aaa aaa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd": "b",
|
||||||
|
"act": "bb",
|
||||||
|
"prompt": "bbb bbb bbb"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
> CSV format
|
||||||
|
|
||||||
|
```csv
|
||||||
|
"cmd","act","prompt"
|
||||||
|
"a","aa","aaa aaa aaa"
|
||||||
|
"b","bb","bbb bbb bbb"
|
||||||
|
```
|
||||||
|
|
||||||
## 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` - Feature Enhancements
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
## 👀 Preview
|
## 👀 Preview
|
||||||
|
|
||||||
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/chat.png" alt="chat">
|
<img width="320" src="./assets/install.png" alt="install"> <img width="320" src="./assets/control-center.png" alt="control center">
|
||||||
<img width="320" src="./assets/export.png" alt="export"> <img width="320" src="./assets/tray.png" alt="tray">
|
<img width="320" src="./assets/export.png" alt="export"> <img width="320" src="./assets/tray.png" alt="tray">
|
||||||
<img width="320" src="./assets/chat-ua.png" alt="user agent"> <img width="320" src="./assets/auto-update.png" alt="auto update">
|
<img width="320" src="./assets/tray-login.png" alt="tray login"> <img width="320" src="./assets/auto-update.png" alt="auto update">
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -92,7 +172,11 @@ cask "popcorn-time", args: { "no-quarantine": true }
|
|||||||
|
|
||||||
### Can't open ChatGPT
|
### Can't open ChatGPT
|
||||||
|
|
||||||
If the application cannot be opened after the upgrade, please try to clear the configuration file, which is in the `~/.chatgpt/*` directory.
|
If you cannot open the application after the upgrade, please try to clear the configuration file, which is in the `~/.chatgpt/*` directory.
|
||||||
|
|
||||||
|
### Out of sync login status between multiple windows
|
||||||
|
|
||||||
|
If you have already logged in in the main window, but the system tray window shows that you are not logged in, you can fix it by restarting the application (`Menu -> Preferences -> Restart ChatGPT`).
|
||||||
|
|
||||||
### Is it safe?
|
### Is it safe?
|
||||||
|
|
||||||
@@ -133,10 +217,19 @@ yarn build
|
|||||||
|
|
||||||
## ❤️ Thanks
|
## ❤️ Thanks
|
||||||
|
|
||||||
- The share buttons code is copied directly from [@liady](https://github.com/liady) extension with minor modifications.
|
- The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications.
|
||||||
|
- Thanks to the [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) repository for inspiring the custom command function for this application.
|
||||||
|
|
||||||
## Related
|
---
|
||||||
|
|
||||||
- [Tauri](https://tauri.app) - Build an optimized, secure, and frontend-independent application for multi-platform deployment.
|
[](https://star-history.com/#lencx/chatgpt&Date)
|
||||||
- [ChatGPT](https://openai.com/blog/chatgpt) - ChatGPT: Optimizing Language Models for Dialogue.
|
|
||||||
- [ChatGPT Export and Share](https://github.com/liady/ChatGPT-pdf) - A Chrome extension for downloading your ChatGPT history to PNG, PDF or creating a sharable link.
|
## 中国用户
|
||||||
|
|
||||||
|
国内用户如果遇到使用问题或者想交流 ChatGPT 技巧,可以关注公众号“浮之静”,发送 “chat” 进群参与讨论。公众号会更新[《Tauri 系列》](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzIzNjE2NTI3NQ==&action=getalbum&album_id=2593843659863752704)文章,技术思考等等,如果对 tauri 开发应用感兴趣可以关注公众号后回复 “tauri” 进技术开发群(想私聊的也可以关注公众号,来添加微信)。开源不易,如果这个项目对你有帮助可以分享给更多人,或者微信扫码打赏。
|
||||||
|
|
||||||
|
<img width="180" src="https://user-images.githubusercontent.com/16164244/207228300-ea5c4688-c916-4c55-a8c3-7f862888f351.png"> <img width="200" src="https://user-images.githubusercontent.com/16164244/207228025-117b5f77-c5d2-48c2-a070-774b7a1596f2.png">
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
|||||||
@@ -1,5 +1,53 @@
|
|||||||
# UPDATE LOG
|
# UPDATE LOG
|
||||||
|
|
||||||
|
## v0.6.0
|
||||||
|
|
||||||
|
fix:
|
||||||
|
- windows show Chinese when upgrading
|
||||||
|
|
||||||
|
feat:
|
||||||
|
- optimize the generated pdf file size
|
||||||
|
- menu added `Sync Prompts`
|
||||||
|
- `Control Center` added `Sync Custom`
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
## v0.5.1
|
||||||
|
|
||||||
|
some optimization
|
||||||
|
|
||||||
|
## v0.5.0
|
||||||
|
|
||||||
|
feat: `Control Center` added `chatgpt-prompts` synchronization
|
||||||
|
|
||||||
|
## v0.4.2
|
||||||
|
|
||||||
|
add chatgpt log (path: `~/.chatgpt/chatgpt.log`)
|
||||||
|
|
||||||
|
## v0.4.1
|
||||||
|
|
||||||
|
fix:
|
||||||
|
- tray window style optimization
|
||||||
|
|
||||||
|
## v0.4.0
|
||||||
|
|
||||||
|
feat:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
fix: can't open ChatGPT
|
||||||
|
|
||||||
|
feat: menu enhancement
|
||||||
|
- the control center of ChatGPT application
|
||||||
|
- open the configuration file directory
|
||||||
|
|
||||||
|
## v0.2.2
|
||||||
|
|
||||||
|
feat:
|
||||||
|
- menu: go to config
|
||||||
|
|
||||||
## v0.2.1
|
## v0.2.1
|
||||||
|
|
||||||
feat: menu optimization
|
feat: menu optimization
|
||||||
@@ -23,7 +71,7 @@ feat: tray window
|
|||||||
## v0.1.6
|
## v0.1.6
|
||||||
|
|
||||||
feat:
|
feat:
|
||||||
- always on top
|
- stay on top
|
||||||
- export ChatGPT history
|
- export ChatGPT history
|
||||||
|
|
||||||
## v0.1.5
|
## v0.1.5
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 773 KiB |
BIN
assets/chat-cmd-1.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/chat-cmd-2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 391 KiB |
BIN
assets/chat.png
|
Before Width: | Height: | Size: 653 KiB |
BIN
assets/chatgpt-cmd.gif
Normal file
|
After Width: | Height: | Size: 5.0 MiB |
BIN
assets/chatgpt.gif
Normal file
|
After Width: | Height: | Size: 9.3 MiB |
BIN
assets/control-center.png
Normal file
|
After Width: | Height: | Size: 500 KiB |
BIN
assets/tray-login.png
Normal file
|
After Width: | Height: | Size: 989 KiB |
3
chat.model.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# ChatGPT Model
|
||||||
|
|
||||||
|
- [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts)
|
||||||
0
dist/.gitkeep
vendored
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ChatGPT</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
package.json
@@ -2,6 +2,8 @@
|
|||||||
"name": "chatgpt",
|
"name": "chatgpt",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev:fe": "vite",
|
||||||
|
"build:fe": "tsc && vite build",
|
||||||
"dev": "yarn tauri dev",
|
"dev": "yarn tauri dev",
|
||||||
"build": "yarn tauri build",
|
"build": "yarn tauri build",
|
||||||
"updater": "tr updater",
|
"updater": "tr updater",
|
||||||
@@ -27,8 +29,29 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/lencx/ChatGPT"
|
"url": "https://github.com/lencx/ChatGPT"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^4.8.0",
|
||||||
|
"@tauri-apps/api": "^1.2.0",
|
||||||
|
"antd": "^5.1.0",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.4.5",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.2.1",
|
"@tauri-apps/cli": "^1.2.2",
|
||||||
"@tauri-release/cli": "^0.2.3"
|
"@tauri-release/cli": "^0.2.3",
|
||||||
|
"@types/lodash": "^4.14.191",
|
||||||
|
"@types/node": "^18.7.10",
|
||||||
|
"@types/react": "^18.0.15",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/uuid": "^9.0.0",
|
||||||
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
|
"sass": "^1.56.2",
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"vite": "^4.0.0",
|
||||||
|
"vite-tsconfig-paths": "^4.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
11
scripts/download.js
vendored
@@ -2,8 +2,8 @@ const fs = require('fs');
|
|||||||
|
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
|
|
||||||
async function init() {
|
async function rewrite(filename) {
|
||||||
const content = fs.readFileSync('README.md', 'utf8').split('\n');
|
const content = fs.readFileSync(filename, 'utf8').split('\n');
|
||||||
const startRe = /<!-- download start -->/;
|
const startRe = /<!-- download start -->/;
|
||||||
const endRe = /<!-- download end -->/;
|
const endRe = /<!-- download end -->/;
|
||||||
|
|
||||||
@@ -20,7 +20,12 @@ async function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync('README.md', content.join('\n'), 'utf8');
|
fs.writeFileSync(filename, content.join('\n'), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
rewrite('README.md');
|
||||||
|
rewrite('README-ZH_CN.md');
|
||||||
}
|
}
|
||||||
|
|
||||||
init().catch(console.error);
|
init().catch(console.error);
|
||||||
@@ -19,6 +19,17 @@ serde_json = "1.0"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.2.2", features = ["api-all", "devtools", "system-tray", "updater"] }
|
tauri = { version = "1.2.2", features = ["api-all", "devtools", "system-tray", "updater"] }
|
||||||
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
|
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
|
||||||
|
log = "0.4.17"
|
||||||
|
csv = "1.1.6"
|
||||||
|
thiserror = "1.0.38"
|
||||||
|
walkdir = "2.3.2"
|
||||||
|
# tokio = { version = "1.23.0", features = ["macros"] }
|
||||||
|
# reqwest = "0.11.13"
|
||||||
|
|
||||||
|
[dependencies.tauri-plugin-log]
|
||||||
|
git = "https://github.com/tauri-apps/tauri-plugin-log"
|
||||||
|
branch = "dev"
|
||||||
|
features = ["colored"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{conf::ChatConfJson, utils};
|
use crate::{conf::ChatConfJson, utils};
|
||||||
use std::fs;
|
use std::{fs, path::PathBuf};
|
||||||
use tauri::{api, command, AppHandle, Manager};
|
use tauri::{api, command, AppHandle, Manager};
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -35,9 +35,8 @@ pub fn get_chat_conf() -> ChatConfJson {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[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)).unwrap();
|
ChatConfJson::amend(&serde_json::json!(data), None).unwrap();
|
||||||
tauri::api::process::restart(&app.env());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -60,3 +59,72 @@ 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]
|
||||||
|
pub fn open_file(path: PathBuf) {
|
||||||
|
utils::open_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, 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();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
use utils::chat_root;
|
||||||
|
|
||||||
|
#[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(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
|
||||||
|
}
|
||||||
|
|||||||
123
src-tauri/src/app/fs_extra.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
// https://github.com/tauri-apps/tauri-plugin-fs-extra/blob/dev/src/lib.rs
|
||||||
|
|
||||||
|
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use serde::{ser::Serializer, Serialize};
|
||||||
|
use std::{
|
||||||
|
path::PathBuf,
|
||||||
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
|
};
|
||||||
|
use tauri::command;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::os::windows::fs::MetadataExt;
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Error {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(self.to_string().as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Permissions {
|
||||||
|
readonly: bool,
|
||||||
|
#[cfg(unix)]
|
||||||
|
mode: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct UnixMetadata {
|
||||||
|
dev: u64,
|
||||||
|
ino: u64,
|
||||||
|
mode: u32,
|
||||||
|
nlink: u64,
|
||||||
|
uid: u32,
|
||||||
|
gid: u32,
|
||||||
|
rdev: u64,
|
||||||
|
blksize: u64,
|
||||||
|
blocks: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Metadata {
|
||||||
|
accessed_at_ms: u64,
|
||||||
|
created_at_ms: u64,
|
||||||
|
modified_at_ms: u64,
|
||||||
|
is_dir: bool,
|
||||||
|
is_file: bool,
|
||||||
|
is_symlink: bool,
|
||||||
|
size: u64,
|
||||||
|
permissions: Permissions,
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[serde(flatten)]
|
||||||
|
unix: UnixMetadata,
|
||||||
|
#[cfg(windows)]
|
||||||
|
file_attributes: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
|
||||||
|
time.map(|t| {
|
||||||
|
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
|
||||||
|
duration_since_epoch.as_millis() as u64
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn metadata(path: PathBuf) -> Result<Metadata> {
|
||||||
|
let metadata = std::fs::metadata(path)?;
|
||||||
|
let file_type = metadata.file_type();
|
||||||
|
let permissions = metadata.permissions();
|
||||||
|
Ok(Metadata {
|
||||||
|
accessed_at_ms: system_time_to_ms(metadata.accessed()),
|
||||||
|
created_at_ms: system_time_to_ms(metadata.created()),
|
||||||
|
modified_at_ms: system_time_to_ms(metadata.modified()),
|
||||||
|
is_dir: file_type.is_dir(),
|
||||||
|
is_file: file_type.is_file(),
|
||||||
|
is_symlink: file_type.is_symlink(),
|
||||||
|
size: metadata.len(),
|
||||||
|
permissions: Permissions {
|
||||||
|
readonly: permissions.readonly(),
|
||||||
|
#[cfg(unix)]
|
||||||
|
mode: permissions.mode(),
|
||||||
|
},
|
||||||
|
#[cfg(unix)]
|
||||||
|
unix: UnixMetadata {
|
||||||
|
dev: metadata.dev(),
|
||||||
|
ino: metadata.ino(),
|
||||||
|
mode: metadata.mode(),
|
||||||
|
nlink: metadata.nlink(),
|
||||||
|
uid: metadata.uid(),
|
||||||
|
gid: metadata.gid(),
|
||||||
|
rdev: metadata.rdev(),
|
||||||
|
blksize: metadata.blksize(),
|
||||||
|
blocks: metadata.blocks(),
|
||||||
|
},
|
||||||
|
#[cfg(windows)]
|
||||||
|
file_attributes: metadata.file_attributes(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[command]
|
||||||
|
// pub async fn exists(path: PathBuf) -> bool {
|
||||||
|
// path.exists()
|
||||||
|
// }
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::window,
|
|
||||||
conf::{self, ChatConfJson},
|
conf::{self, ChatConfJson},
|
||||||
utils,
|
utils,
|
||||||
};
|
};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
utils::assets::EmbeddedAssets, AboutMetadata, AppHandle, Context, CustomMenuItem, Manager,
|
AboutMetadata, AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray,
|
||||||
Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowMenuEvent,
|
SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowMenuEvent,
|
||||||
};
|
};
|
||||||
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
|
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
|
||||||
|
|
||||||
// --- Menu
|
// --- Menu
|
||||||
pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
|
pub fn init() -> Menu {
|
||||||
let chat_conf = ChatConfJson::get_chat_conf();
|
let chat_conf = ChatConfJson::get_chat_conf();
|
||||||
let name = &context.package_info().name;
|
let name = "ChatGPT";
|
||||||
let app_menu = Submenu::new(
|
let app_menu = Submenu::new(
|
||||||
name,
|
name,
|
||||||
Menu::new()
|
Menu::new()
|
||||||
@@ -26,18 +25,18 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
|
|||||||
.add_native_item(MenuItem::Quit),
|
.add_native_item(MenuItem::Quit),
|
||||||
);
|
);
|
||||||
|
|
||||||
let always_on_top = CustomMenuItem::new("always_on_top".to_string(), "Always On Top")
|
let stay_on_top =
|
||||||
.accelerator("CmdOrCtrl+T");
|
CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T");
|
||||||
let titlebar =
|
let titlebar =
|
||||||
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
|
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
|
||||||
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
|
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
|
||||||
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
|
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
|
||||||
let is_dark = chat_conf.theme == "Dark";
|
let is_dark = chat_conf.theme == "Dark";
|
||||||
|
|
||||||
let always_on_top_menu = if chat_conf.always_on_top {
|
let stay_on_top_menu = if chat_conf.stay_on_top {
|
||||||
always_on_top.selected()
|
stay_on_top.selected()
|
||||||
} else {
|
} else {
|
||||||
always_on_top
|
stay_on_top
|
||||||
};
|
};
|
||||||
let titlebar_menu = if chat_conf.titlebar {
|
let titlebar_menu = if chat_conf.titlebar {
|
||||||
titlebar.selected()
|
titlebar.selected()
|
||||||
@@ -48,6 +47,10 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
|
|||||||
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")
|
||||||
|
.accelerator("CmdOrCtrl+Shift+P")
|
||||||
|
.into(),
|
||||||
|
MenuItem::Separator.into(),
|
||||||
Submenu::new(
|
Submenu::new(
|
||||||
"Theme",
|
"Theme",
|
||||||
Menu::new()
|
Menu::new()
|
||||||
@@ -63,31 +66,29 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
always_on_top_menu.into(),
|
stay_on_top_menu.into(),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
titlebar_menu.into(),
|
titlebar_menu.into(),
|
||||||
MenuItem::Separator.into(),
|
#[cfg(target_os = "macos")]
|
||||||
// fix: Checking if the site connection is secure
|
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
|
||||||
// @link: https://github.com/lencx/ChatGPT/issues/17
|
|
||||||
CustomMenuItem::new("user_agent".to_string(), "User Agent")
|
|
||||||
.accelerator("CmdOrCtrl+U")
|
|
||||||
.into(),
|
|
||||||
CustomMenuItem::new("switch_origin".to_string(), "Switch Origin")
|
|
||||||
.accelerator("CmdOrCtrl+O")
|
|
||||||
.into(),
|
|
||||||
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
|
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
|
||||||
.accelerator("CmdOrCtrl+J")
|
.accelerator("CmdOrCtrl+J")
|
||||||
.into(),
|
.into(),
|
||||||
MenuItem::Separator.into(),
|
MenuItem::Separator.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")
|
CustomMenuItem::new("clear_conf".to_string(), "Clear Config")
|
||||||
.accelerator("CmdOrCtrl+D")
|
.accelerator("CmdOrCtrl+Shift+D")
|
||||||
.into(),
|
.into(),
|
||||||
CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
|
CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
|
||||||
.accelerator("CmdOrCtrl+Shift+R")
|
.accelerator("CmdOrCtrl+Shift+R")
|
||||||
.into(),
|
.into(),
|
||||||
MenuItem::Separator.into(),
|
MenuItem::Separator.into(),
|
||||||
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
|
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
|
||||||
.accelerator("CmdOrCtrl+Z")
|
.accelerator("CmdOrCtrl+Shift+A")
|
||||||
.into(),
|
.into(),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
@@ -122,7 +123,6 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
|
|||||||
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen")
|
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen")
|
||||||
.accelerator("CmdOrCtrl+Down"),
|
.accelerator("CmdOrCtrl+Down"),
|
||||||
)
|
)
|
||||||
.add_native_item(MenuItem::Zoom)
|
|
||||||
.add_native_item(MenuItem::Separator)
|
.add_native_item(MenuItem::Separator)
|
||||||
.add_item(
|
.add_item(
|
||||||
CustomMenuItem::new("reload".to_string(), "Refresh the Screen")
|
CustomMenuItem::new("reload".to_string(), "Refresh the Screen")
|
||||||
@@ -130,9 +130,20 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let window_menu = Submenu::new(
|
||||||
|
"Window",
|
||||||
|
Menu::new()
|
||||||
|
.add_native_item(MenuItem::Minimize)
|
||||||
|
.add_native_item(MenuItem::Zoom),
|
||||||
|
);
|
||||||
|
|
||||||
let help_menu = Submenu::new(
|
let help_menu = Submenu::new(
|
||||||
"Help",
|
"Help",
|
||||||
Menu::new()
|
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("update_log".to_string(), "Update Log"))
|
||||||
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
|
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
|
||||||
.add_item(
|
.add_item(
|
||||||
@@ -146,6 +157,7 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
|
|||||||
.add_submenu(preferences_menu)
|
.add_submenu(preferences_menu)
|
||||||
.add_submenu(edit_menu)
|
.add_submenu(edit_menu)
|
||||||
.add_submenu(view_menu)
|
.add_submenu(view_menu)
|
||||||
|
.add_submenu(window_menu)
|
||||||
.add_submenu(help_menu)
|
.add_submenu(help_menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,15 +174,37 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
|
|||||||
|
|
||||||
match menu_id {
|
match menu_id {
|
||||||
// Preferences
|
// Preferences
|
||||||
|
"control_center" => app.get_window("main").unwrap().show().unwrap(),
|
||||||
"restart" => tauri::api::process::restart(&app.env()),
|
"restart" => tauri::api::process::restart(&app.env()),
|
||||||
"inject_script" => open(&app, script_path),
|
"inject_script" => open(&app, script_path),
|
||||||
|
"go_conf" => utils::open_file(utils::chat_root()),
|
||||||
"clear_conf" => utils::clear_conf(&app),
|
"clear_conf" => utils::clear_conf(&app),
|
||||||
"switch_origin" => window::origin_window(&app),
|
|
||||||
"user_agent" => window::ua_window(&app),
|
|
||||||
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
|
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
|
||||||
|
"sync_prompts" => {
|
||||||
|
tauri::api::dialog::ask(
|
||||||
|
app.get_window("main").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("main")
|
||||||
|
.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" => {
|
"titlebar" => {
|
||||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||||
ChatConfJson::amend(&serde_json::json!({ "titlebar": !chat_conf.titlebar })).unwrap();
|
ChatConfJson::amend(
|
||||||
|
&serde_json::json!({ "titlebar": !chat_conf.titlebar }),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
tauri::api::process::restart(&app.env());
|
tauri::api::process::restart(&app.env());
|
||||||
}
|
}
|
||||||
"theme_light" | "theme_dark" => {
|
"theme_light" | "theme_dark" => {
|
||||||
@@ -179,18 +213,17 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
|
|||||||
} else {
|
} else {
|
||||||
"Light"
|
"Light"
|
||||||
};
|
};
|
||||||
ChatConfJson::amend(&serde_json::json!({ "theme": theme })).unwrap();
|
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap();
|
||||||
tauri::api::process::restart(&app.env());
|
|
||||||
}
|
}
|
||||||
"always_on_top" => {
|
"stay_on_top" => {
|
||||||
let mut always_on_top = state.always_on_top.lock().unwrap();
|
let mut stay_on_top = state.stay_on_top.lock().unwrap();
|
||||||
*always_on_top = !*always_on_top;
|
*stay_on_top = !*stay_on_top;
|
||||||
menu_handle
|
menu_handle
|
||||||
.get_item(menu_id)
|
.get_item(menu_id)
|
||||||
.set_selected(*always_on_top)
|
.set_selected(*stay_on_top)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
win.set_always_on_top(*always_on_top).unwrap();
|
win.set_always_on_top(*stay_on_top).unwrap();
|
||||||
ChatConfJson::amend(&serde_json::json!({ "always_on_top": *always_on_top })).unwrap();
|
ChatConfJson::amend(&serde_json::json!({ "stay_on_top": *stay_on_top }), None).unwrap();
|
||||||
}
|
}
|
||||||
// View
|
// View
|
||||||
"reload" => win.eval("window.location.reload()").unwrap(),
|
"reload" => win.eval("window.location.reload()").unwrap(),
|
||||||
@@ -214,6 +247,7 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
|
|||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
// Help
|
// Help
|
||||||
|
"chatgpt_log" => utils::open_file(utils::chat_root().join("chatgpt.log")),
|
||||||
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
|
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
|
||||||
"report_bug" => open(&app, conf::ISSUES_URL.to_string()),
|
"report_bug" => open(&app, conf::ISSUES_URL.to_string()),
|
||||||
"dev_tools" => {
|
"dev_tools" => {
|
||||||
@@ -226,24 +260,67 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
|
|||||||
|
|
||||||
// --- SystemTray Menu
|
// --- SystemTray Menu
|
||||||
pub fn tray_menu() -> SystemTray {
|
pub fn tray_menu() -> SystemTray {
|
||||||
SystemTray::new().with_menu(SystemTrayMenu::new())
|
SystemTray::new().with_menu(
|
||||||
|
SystemTrayMenu::new()
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"control_center".to_string(),
|
||||||
|
"Control Center",
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"show_dock_icon".to_string(),
|
||||||
|
"Show Dock Icon",
|
||||||
|
))
|
||||||
|
.add_item(CustomMenuItem::new(
|
||||||
|
"hide_dock_icon".to_string(),
|
||||||
|
"Hide Dock Icon",
|
||||||
|
))
|
||||||
|
.add_native_item(SystemTrayMenuItem::Separator)
|
||||||
|
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SystemTray Event
|
// --- SystemTray Event
|
||||||
pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
|
pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
|
||||||
let core_win = handle.get_window("core").unwrap();
|
|
||||||
on_tray_event(handle, &event);
|
on_tray_event(handle, &event);
|
||||||
|
|
||||||
if let SystemTrayEvent::LeftClick { .. } = event {
|
let app = handle.clone();
|
||||||
core_win.minimize().unwrap();
|
|
||||||
let mini_win = handle.get_window("mini").unwrap();
|
|
||||||
mini_win.move_window(Position::TrayCenter).unwrap();
|
|
||||||
|
|
||||||
if mini_win.is_visible().unwrap() {
|
match event {
|
||||||
mini_win.hide().unwrap();
|
SystemTrayEvent::LeftClick { .. } => {
|
||||||
} else {
|
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||||
mini_win.show().unwrap();
|
|
||||||
|
if !chat_conf.hide_dock_icon {
|
||||||
|
let core_win = handle.get_window("core").unwrap();
|
||||||
|
core_win.minimize().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let tray_win = handle.get_window("tray").unwrap();
|
||||||
|
tray_win.move_window(Position::TrayCenter).unwrap();
|
||||||
|
|
||||||
|
if tray_win.is_visible().unwrap() {
|
||||||
|
tray_win.hide().unwrap();
|
||||||
|
} else {
|
||||||
|
tray_win.show().unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||||
|
"control_center" => app.get_window("main").unwrap().show().unwrap(),
|
||||||
|
"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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"quit" => std::process::exit(0),
|
||||||
|
_ => (),
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod cmd;
|
pub mod cmd;
|
||||||
|
pub mod fs_extra;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|||||||
@@ -5,40 +5,57 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
|
|||||||
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.origin.to_string();
|
||||||
let theme = ChatConfJson::theme();
|
let theme = ChatConfJson::theme();
|
||||||
window::mini_window(&app.app_handle());
|
let handle = app.app_handle();
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
std::thread::spawn(move || {
|
||||||
WindowBuilder::new(app, "core", WindowUrl::App(url.into()))
|
window::tray_window(&handle);
|
||||||
.resizable(true)
|
});
|
||||||
.fullscreen(false)
|
|
||||||
.inner_size(800.0, 600.0)
|
|
||||||
.hidden_title(true)
|
|
||||||
.theme(theme)
|
|
||||||
.always_on_top(chat_conf.always_on_top)
|
|
||||||
.title_bar_style(ChatConfJson::titlebar())
|
|
||||||
.initialization_script(&utils::user_script())
|
|
||||||
.initialization_script(include_str!("../assets/html2canvas.js"))
|
|
||||||
.initialization_script(include_str!("../assets/jspdf.js"))
|
|
||||||
.initialization_script(include_str!("../assets/core.js"))
|
|
||||||
.initialization_script(include_str!("../assets/export.js"))
|
|
||||||
.user_agent(&chat_conf.ua_pc)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
if chat_conf.hide_dock_icon {
|
||||||
WindowBuilder::new(app, "core", WindowUrl::App(url.into()))
|
#[cfg(target_os = "macos")]
|
||||||
.title("ChatGPT")
|
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||||
.resizable(true)
|
} else {
|
||||||
.fullscreen(false)
|
let app = app.handle();
|
||||||
.inner_size(800.0, 600.0)
|
std::thread::spawn(move || {
|
||||||
.theme(theme)
|
#[cfg(target_os = "macos")]
|
||||||
.always_on_top(chat_conf.always_on_top)
|
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
|
||||||
.initialization_script(&utils::user_script())
|
.title("ChatGPT")
|
||||||
.initialization_script(include_str!("../assets/html2canvas.js"))
|
.resizable(true)
|
||||||
.initialization_script(include_str!("../assets/jspdf.js"))
|
.fullscreen(false)
|
||||||
.initialization_script(include_str!("../assets/core.js"))
|
.inner_size(800.0, 600.0)
|
||||||
.initialization_script(include_str!("../assets/export.js"))
|
.hidden_title(true)
|
||||||
.user_agent(&chat_conf.ua_pc)
|
.theme(theme)
|
||||||
.build()?;
|
.always_on_top(chat_conf.stay_on_top)
|
||||||
|
.title_bar_style(ChatConfJson::titlebar())
|
||||||
|
.initialization_script(&utils::user_script())
|
||||||
|
.initialization_script(include_str!("../assets/html2canvas.js"))
|
||||||
|
.initialization_script(include_str!("../assets/jspdf.js"))
|
||||||
|
.initialization_script(include_str!("../assets/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!("../assets/html2canvas.js"))
|
||||||
|
.initialization_script(include_str!("../assets/jspdf.js"))
|
||||||
|
.initialization_script(include_str!("../assets/core.js"))
|
||||||
|
.initialization_script(include_str!("../assets/export.js"))
|
||||||
|
.initialization_script(include_str!("../assets/cmd.js"))
|
||||||
|
.user_agent(&chat_conf.ua_window)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,30 @@
|
|||||||
use crate::{conf, utils};
|
use crate::{conf, utils};
|
||||||
use tauri::{utils::config::WindowUrl, window::WindowBuilder};
|
use tauri::{utils::config::WindowUrl, window::WindowBuilder};
|
||||||
|
|
||||||
pub fn mini_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();
|
||||||
|
|
||||||
WindowBuilder::new(handle, "mini", WindowUrl::App(chat_conf.origin.into()))
|
std::thread::spawn(move || {
|
||||||
.resizable(false)
|
WindowBuilder::new(&app, "tray", WindowUrl::App(chat_conf.origin.into()))
|
||||||
.fullscreen(false)
|
.title("ChatGPT")
|
||||||
.inner_size(360.0, 540.0)
|
.resizable(false)
|
||||||
.decorations(false)
|
.fullscreen(false)
|
||||||
.always_on_top(true)
|
.inner_size(360.0, 540.0)
|
||||||
.theme(theme)
|
.decorations(false)
|
||||||
.initialization_script(&utils::user_script())
|
.always_on_top(true)
|
||||||
.initialization_script(include_str!("../assets/html2canvas.js"))
|
.theme(theme)
|
||||||
.initialization_script(include_str!("../assets/jspdf.js"))
|
.initialization_script(&utils::user_script())
|
||||||
.initialization_script(include_str!("../assets/core.js"))
|
.initialization_script(include_str!("../assets/html2canvas.js"))
|
||||||
.initialization_script(include_str!("../assets/export.js"))
|
.initialization_script(include_str!("../assets/jspdf.js"))
|
||||||
.user_agent(&chat_conf.ua_phone)
|
.initialization_script(include_str!("../assets/core.js"))
|
||||||
.build()
|
.initialization_script(include_str!("../assets/export.js"))
|
||||||
.unwrap()
|
.initialization_script(include_str!("../assets/cmd.js"))
|
||||||
.hide()
|
.user_agent(&chat_conf.ua_tray)
|
||||||
.unwrap();
|
.build()
|
||||||
}
|
.unwrap()
|
||||||
|
.hide()
|
||||||
pub fn origin_window(handle: &tauri::AppHandle) {
|
.unwrap();
|
||||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
});
|
||||||
WindowBuilder::new(handle, "origin", WindowUrl::App(chat_conf.origin.into()))
|
|
||||||
.resizable(false)
|
|
||||||
.fullscreen(false)
|
|
||||||
.inner_size(400.0, 300.0)
|
|
||||||
.always_on_top(true)
|
|
||||||
.decorations(false)
|
|
||||||
.initialization_script(include_str!("../assets/core.js"))
|
|
||||||
.initialization_script(include_str!("../assets/origin.js"))
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ua_window(handle: &tauri::AppHandle) {
|
|
||||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
|
||||||
WindowBuilder::new(handle, "ua", WindowUrl::App(chat_conf.origin.into()))
|
|
||||||
.resizable(false)
|
|
||||||
.fullscreen(false)
|
|
||||||
.inner_size(540.0, 480.0)
|
|
||||||
.always_on_top(true)
|
|
||||||
.decorations(false)
|
|
||||||
.initialization_script(include_str!("../assets/core.js"))
|
|
||||||
.initialization_script(include_str!("../assets/ua.js"))
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|||||||
212
src-tauri/src/assets/cmd.js
vendored
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// *** 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 #d8d8d8;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
.chat-model-cmd-list .cmd-item {
|
||||||
|
font-size: 12px;
|
||||||
|
border-bottom: solid 1px #888;
|
||||||
|
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 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;
|
||||||
|
}`;
|
||||||
|
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 = '40px';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('form').appendChild(modelDom);
|
||||||
|
const itemDom = (v) => `<div class="cmd-item" title="${v.prompt}" data-prompt="${encodeURIComponent(v.prompt)}"><b title="${v.cmd}">/${v.cmd}</b><i>${v.act}</i></div>`;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
const data = searchInput.value.split('|->');
|
||||||
|
if (data[1]?.trim()) {
|
||||||
|
window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${data[1]?.trim()}}`);
|
||||||
|
window.__CHAT_MODEL_STATUS__ = 2;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// input text
|
||||||
|
if (window.__CHAT_MODEL_STATUS__ === 2 && event.keyCode === 9) {
|
||||||
|
console.log('«110» /src/assets/cmd.js ~> ', __CHAT_MODEL_STATUS__);
|
||||||
|
|
||||||
|
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__) {
|
||||||
|
const data = searchInput.value.split('|->');
|
||||||
|
if (data[1]?.trim()) {
|
||||||
|
window.__CHAT_MODEL_CMD_PROMPT__ = window.__CHAT_MODEL_CMD_PROMPT__?.replace(/\{([^{}]*)\}/, `{${data[1]?.trim()}}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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', (event) => {
|
||||||
|
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 === '/') {
|
||||||
|
modelDom.innerHTML = `<div>${data.map(itemDom).join('')}</div>`;
|
||||||
|
window.__CHAT_MODEL_CMD_PROMPT__ = data[0]?.prompt.trim();
|
||||||
|
window.__CHAT_MODEL_CMD__ = data[0]?.cmd.trim();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd));
|
||||||
|
if (result.length > 0) {
|
||||||
|
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
|
||||||
|
window.__CHAT_MODEL_CMD_PROMPT__ = result[0]?.prompt.trim();
|
||||||
|
window.__CHAT_MODEL_CMD__ = result[0]?.cmd.trim();
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
3
src-tauri/src/assets/core.js
vendored
@@ -1,4 +1,5 @@
|
|||||||
// *** Core Script - IPC ***
|
// *** Core Script - IPC ***
|
||||||
|
|
||||||
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
|
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
|
||||||
function transformCallback(callback = () => {}, once = false) {
|
function transformCallback(callback = () => {}, once = false) {
|
||||||
const identifier = uid();
|
const identifier = uid();
|
||||||
@@ -40,7 +41,7 @@ window.invoke = invoke;
|
|||||||
window.transformCallback = transformCallback;
|
window.transformCallback = transformCallback;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
if (__TAURI_METADATA__.__currentWindow.label === 'mini') {
|
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
|
||||||
document.getElementsByTagName('html')[0].style['font-size'] = '70%';
|
document.getElementsByTagName('html')[0].style['font-size'] = '70%';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
125
src-tauri/src/assets/export.js
vendored
@@ -1,6 +1,7 @@
|
|||||||
// *** Core Script - Export ***
|
// *** Core Script - Export ***
|
||||||
// @ref: https://github.com/liady/ChatGPT-pdf/blob/main/src/content_script.js
|
// @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 chatConf = await invoke('get_chat_conf') || {};
|
const chatConf = await invoke('get_chat_conf') || {};
|
||||||
if (window.buttonsInterval) {
|
if (window.buttonsInterval) {
|
||||||
@@ -11,14 +12,15 @@ async function init() {
|
|||||||
if (!actionsArea) {
|
if (!actionsArea) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const buttons = actionsArea.querySelectorAll("button");
|
if (shouldAddButtons(actionsArea)) {
|
||||||
const hasTryAgainButton = Array.from(buttons).some((button) => {
|
let TryAgainButton = actionsArea.querySelector("button");
|
||||||
return !button.id?.includes("download");
|
if (!TryAgainButton) {
|
||||||
});
|
const parentNode = document.createElement("div");
|
||||||
if (hasTryAgainButton && buttons.length === 1) {
|
parentNode.innerHTML = buttonOuterHTMLFallback;
|
||||||
const TryAgainButton = actionsArea.querySelector("button");
|
TryAgainButton = parentNode.querySelector("button");
|
||||||
|
}
|
||||||
addActionsButtons(actionsArea, TryAgainButton, chatConf);
|
addActionsButtons(actionsArea, TryAgainButton, chatConf);
|
||||||
} else if (!hasTryAgainButton) {
|
} else if (shouldRemoveButtons()) {
|
||||||
removeButtons();
|
removeButtons();
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
@@ -29,32 +31,42 @@ const Format = {
|
|||||||
PDF: "pdf",
|
PDF: "pdf",
|
||||||
};
|
};
|
||||||
|
|
||||||
function addActionsButtons(actionsArea, TryAgainButton, chatConf) {
|
function shouldRemoveButtons() {
|
||||||
const downloadButton = TryAgainButton.cloneNode(true);
|
const isOpenScreen = document.querySelector("h1.text-4xl");
|
||||||
downloadButton.id = "download-png-button";
|
if(isOpenScreen){
|
||||||
downloadButton.innerText = "Generate PNG";
|
return true;
|
||||||
downloadButton.onclick = () => {
|
|
||||||
downloadThread();
|
|
||||||
};
|
|
||||||
actionsArea.appendChild(downloadButton);
|
|
||||||
|
|
||||||
const downloadPdfButton = TryAgainButton.cloneNode(true);
|
|
||||||
downloadPdfButton.id = "download-pdf-button";
|
|
||||||
downloadPdfButton.innerText = "Download PDF";
|
|
||||||
downloadPdfButton.onclick = () => {
|
|
||||||
downloadThread({ as: Format.PDF });
|
|
||||||
};
|
|
||||||
actionsArea.appendChild(downloadPdfButton);
|
|
||||||
|
|
||||||
if (new RegExp('//chat.openai.com').test(chatConf.origin)) {
|
|
||||||
const exportHtml = TryAgainButton.cloneNode(true);
|
|
||||||
exportHtml.id = "download-html-button";
|
|
||||||
exportHtml.innerText = "Share Link";
|
|
||||||
exportHtml.onclick = () => {
|
|
||||||
sendRequest();
|
|
||||||
};
|
|
||||||
actionsArea.appendChild(exportHtml);
|
|
||||||
}
|
}
|
||||||
|
const inConversation = document.querySelector("form button>div");
|
||||||
|
if(inConversation){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldAddButtons(actionsArea) {
|
||||||
|
// first, check if there's a "Try Again" button and no other buttons
|
||||||
|
const buttons = actionsArea.querySelectorAll("button");
|
||||||
|
const hasTryAgainButton = Array.from(buttons).some((button) => {
|
||||||
|
return !button.id?.includes("download");
|
||||||
|
});
|
||||||
|
if (hasTryAgainButton && buttons.length === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, check if open screen is not visible
|
||||||
|
const isOpenScreen = document.querySelector("h1.text-4xl");
|
||||||
|
if (isOpenScreen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the conversation is finished and there are no share buttons
|
||||||
|
const finishedConversation = document.querySelector("form button>svg");
|
||||||
|
const hasShareButtons = actionsArea.querySelectorAll("button[share-ext]");
|
||||||
|
if (finishedConversation && !hasShareButtons.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeButtons() {
|
function removeButtons() {
|
||||||
@@ -72,28 +84,42 @@ function removeButtons() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addActionsButtons(actionsArea, TryAgainButton) {
|
||||||
|
const downloadButton = TryAgainButton.cloneNode(true);
|
||||||
|
downloadButton.id = "download-png-button";
|
||||||
|
downloadButton.setAttribute("share-ext", "true");
|
||||||
|
downloadButton.innerText = "Generate PNG";
|
||||||
|
downloadButton.onclick = () => {
|
||||||
|
downloadThread();
|
||||||
|
};
|
||||||
|
actionsArea.appendChild(downloadButton);
|
||||||
|
const downloadPdfButton = TryAgainButton.cloneNode(true);
|
||||||
|
downloadPdfButton.id = "download-pdf-button";
|
||||||
|
downloadButton.setAttribute("share-ext", "true");
|
||||||
|
downloadPdfButton.innerText = "Download PDF";
|
||||||
|
downloadPdfButton.onclick = () => {
|
||||||
|
downloadThread({ as: Format.PDF });
|
||||||
|
};
|
||||||
|
actionsArea.appendChild(downloadPdfButton);
|
||||||
|
const exportHtml = TryAgainButton.cloneNode(true);
|
||||||
|
exportHtml.id = "download-html-button";
|
||||||
|
downloadButton.setAttribute("share-ext", "true");
|
||||||
|
exportHtml.innerText = "Share Link";
|
||||||
|
exportHtml.onclick = () => {
|
||||||
|
sendRequest();
|
||||||
|
};
|
||||||
|
actionsArea.appendChild(exportHtml);
|
||||||
|
}
|
||||||
|
|
||||||
function downloadThread({ as = Format.PNG } = {}) {
|
function downloadThread({ as = Format.PNG } = {}) {
|
||||||
const elements = new Elements();
|
const elements = new Elements();
|
||||||
elements.fixLocation();
|
elements.fixLocation();
|
||||||
const pixelRatio = window.devicePixelRatio;
|
const pixelRatio = window.devicePixelRatio;
|
||||||
const minRatio = as === Format.PDF ? 2 : 2.5;
|
const minRatio = as === Format.PDF ? 2 : 2.5;
|
||||||
window.devicePixelRatio = Math.max(pixelRatio, minRatio);
|
window.devicePixelRatio = Math.max(pixelRatio, minRatio);
|
||||||
|
|
||||||
html2canvas(elements.thread, {
|
html2canvas(elements.thread, {
|
||||||
letterRendering: true,
|
letterRendering: true,
|
||||||
onclone: function (cloneDoc) {
|
|
||||||
//Make small fix of position to all the text containers
|
|
||||||
let listOfTexts = cloneDoc.getElementsByClassName("min-h-[20px]");
|
|
||||||
Array.from(listOfTexts).forEach((text) => {
|
|
||||||
text.style.position = "relative";
|
|
||||||
text.style.top = "-8px";
|
|
||||||
});
|
|
||||||
|
|
||||||
//Delete copy button from code blocks
|
|
||||||
let listOfCopyBtns = cloneDoc.querySelectorAll("button.flex");
|
|
||||||
Array.from(listOfCopyBtns).forEach(
|
|
||||||
(btn) => (btn.style.visibility = "hidden")
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}).then(async function (canvas) {
|
}).then(async function (canvas) {
|
||||||
elements.restoreLocation();
|
elements.restoreLocation();
|
||||||
window.devicePixelRatio = pixelRatio;
|
window.devicePixelRatio = pixelRatio;
|
||||||
@@ -126,7 +152,7 @@ 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);
|
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST');
|
||||||
|
|
||||||
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)) });
|
invoke('download', { name: `chatgpt-${Date.now()}.pdf`, blob: Array.from(new Uint8Array(data)) });
|
||||||
@@ -166,6 +192,8 @@ class Elements {
|
|||||||
img.setAttribute("srcset_old", srcset);
|
img.setAttribute("srcset_old", srcset);
|
||||||
img.setAttribute("srcset", "");
|
img.setAttribute("srcset", "");
|
||||||
});
|
});
|
||||||
|
//Fix to the text shifting down when generating the canvas
|
||||||
|
document.body.style.lineHeight = "0.5";
|
||||||
}
|
}
|
||||||
restoreLocation() {
|
restoreLocation() {
|
||||||
this.hiddens.forEach((el) => {
|
this.hiddens.forEach((el) => {
|
||||||
@@ -182,6 +210,7 @@ class Elements {
|
|||||||
img.setAttribute("srcset", srcset);
|
img.setAttribute("srcset", srcset);
|
||||||
img.setAttribute("srcset_old", "");
|
img.setAttribute("srcset_old", "");
|
||||||
});
|
});
|
||||||
|
document.body.style.lineHeight = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
77
src-tauri/src/assets/origin.js
vendored
@@ -1,77 +0,0 @@
|
|||||||
function init() {
|
|
||||||
document.body.innerHTML = `<style>
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
all: unset;
|
|
||||||
width: 280px;
|
|
||||||
height: 30px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 0 5px;
|
|
||||||
border: solid 2px #d8d8d8;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 5px !important;
|
|
||||||
color: #4a4a4a;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
all: unset;
|
|
||||||
height: 30px;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 0 10px;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 0 5px;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#cancel {
|
|
||||||
background-color: #999;
|
|
||||||
}
|
|
||||||
#confirm {
|
|
||||||
background-color: #10a37f;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<h3>Switch Origin</h3>
|
|
||||||
<input id="input" type="text" autocapitalize="off" autocomplete="off" spellcheck="false" autofocus placeholder="https://chat.openai.com" />
|
|
||||||
<div class="btns">
|
|
||||||
<button id="cancel">Cancel</button>
|
|
||||||
<button id="confirm">Confirm</button>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const srcipt = document.createElement('script');
|
|
||||||
srcipt.innerHTML = `const input = document.getElementById('input');
|
|
||||||
const cancelBtn = document.getElementById('cancel');
|
|
||||||
const confirmBtn = document.getElementById('confirm');
|
|
||||||
cancelBtn.addEventListener('click', () => {
|
|
||||||
window.invoke('form_cancel', { label: 'origin', title: 'Switch Origin', msg: 'Are you sure you want to cancel editing?' });
|
|
||||||
})
|
|
||||||
confirmBtn.addEventListener('click', () => {
|
|
||||||
if (/^https?:\\/\\//.test(input.value)) {
|
|
||||||
window.invoke('form_confirm', { data: { origin: input.value } });
|
|
||||||
} else {
|
|
||||||
window.invoke('form_msg', { label: 'origin', title: 'Switch Origin', msg: 'Invalid URL!' });
|
|
||||||
}
|
|
||||||
})`;
|
|
||||||
document.head.appendChild(srcipt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// run init
|
|
||||||
if (
|
|
||||||
document.readyState === "complete" ||
|
|
||||||
document.readyState === "interactive"
|
|
||||||
) {
|
|
||||||
init();
|
|
||||||
} else {
|
|
||||||
document.addEventListener("DOMContentLoaded", init);
|
|
||||||
}
|
|
||||||
89
src-tauri/src/assets/ua.js
vendored
@@ -1,89 +0,0 @@
|
|||||||
function init() {
|
|
||||||
document.body.innerHTML = `<style>
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: Söhne,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,Helvetica Neue,Arial,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
all: unset;
|
|
||||||
width: 300px;
|
|
||||||
height: 60px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
border: solid 2px #d8d8d8;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 5px !important;
|
|
||||||
color: #4a4a4a;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
all: unset;
|
|
||||||
height: 30px;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 0 10px;
|
|
||||||
line-height: 30px;
|
|
||||||
margin: 0 5px;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
#cancel {
|
|
||||||
background-color: #999;
|
|
||||||
}
|
|
||||||
#confirm {
|
|
||||||
background-color: #10a37f;
|
|
||||||
}
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
width: 120px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<h3>User Agent</h3>
|
|
||||||
<div class="item">
|
|
||||||
<label>Main Window (PC)</label>
|
|
||||||
<textarea id="ua_pc" type="text" autocapitalize="off" autocomplete="off" spellcheck="false" autofocus placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<label>Tray Window (Phone)</label>
|
|
||||||
<textarea id="ua_phone" type="text" autocapitalize="off" autocomplete="off" spellcheck="false" autofocus placeholder="Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS ..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="btns">
|
|
||||||
<button id="cancel">Cancel</button>
|
|
||||||
<button id="confirm">Confirm</button>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const srcipt = document.createElement('script');
|
|
||||||
srcipt.innerHTML = `const ua_pc = document.getElementById('ua_pc');
|
|
||||||
const ua_phone = document.getElementById('ua_phone');
|
|
||||||
const cancelBtn = document.getElementById('cancel');
|
|
||||||
const confirmBtn = document.getElementById('confirm');
|
|
||||||
cancelBtn.addEventListener('click', () => {
|
|
||||||
window.invoke('form_cancel', { label: 'ua', title: 'User Agent', msg: 'Are you sure you want to cancel editing?' });
|
|
||||||
})
|
|
||||||
confirmBtn.addEventListener('click', () => {
|
|
||||||
window.invoke('form_confirm', { data: { ua_pc: ua_pc.value, ua_phone: ua_phone.value } });
|
|
||||||
})`;
|
|
||||||
document.head.appendChild(srcipt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// run init
|
|
||||||
if (
|
|
||||||
document.readyState === "complete" ||
|
|
||||||
document.readyState === "interactive"
|
|
||||||
) {
|
|
||||||
init();
|
|
||||||
} else {
|
|
||||||
document.addEventListener("DOMContentLoaded", init);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ use crate::utils::{chat_root, create_file, exists};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{collections::BTreeMap, fs, path::PathBuf, sync::Mutex};
|
use std::{collections::BTreeMap, fs, path::PathBuf, sync::Mutex};
|
||||||
use tauri::Theme;
|
use tauri::{Manager, Theme};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use tauri::TitleBarStyle;
|
use tauri::TitleBarStyle;
|
||||||
@@ -14,45 +14,52 @@ 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 DEFAULT_CHAT_CONF: &str = r#"{
|
pub const DEFAULT_CHAT_CONF: &str = r#"{
|
||||||
"always_on_top": false,
|
"stay_on_top": false,
|
||||||
"theme": "Light",
|
"theme": "Light",
|
||||||
"titlebar": true,
|
"titlebar": true,
|
||||||
|
"hide_dock_icon": false,
|
||||||
"default_origin": "https://chat.openai.com",
|
"default_origin": "https://chat.openai.com",
|
||||||
"origin": "https://chat.openai.com",
|
"origin": "https://chat.openai.com",
|
||||||
"ua_pc": "",
|
"ua_window": "",
|
||||||
"ua_phone": ""
|
"ua_tray": ""
|
||||||
}"#;
|
}"#;
|
||||||
pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
|
pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
|
||||||
"always_on_top": false,
|
"stay_on_top": false,
|
||||||
"theme": "Light",
|
"theme": "Light",
|
||||||
"titlebar": false,
|
"titlebar": false,
|
||||||
|
"hide_dock_icon": false,
|
||||||
"default_origin": "https://chat.openai.com",
|
"default_origin": "https://chat.openai.com",
|
||||||
"origin": "https://chat.openai.com",
|
"origin": "https://chat.openai.com",
|
||||||
"ua_pc": "",
|
"ua_window": "",
|
||||||
"ua_phone": ""
|
"ua_tray": ""
|
||||||
}"#;
|
}"#;
|
||||||
|
|
||||||
pub struct ChatState {
|
pub struct ChatState {
|
||||||
pub always_on_top: Mutex<bool>,
|
pub stay_on_top: Mutex<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatState {
|
impl ChatState {
|
||||||
pub fn default(chat_conf: ChatConfJson) -> Self {
|
pub fn default(chat_conf: ChatConfJson) -> Self {
|
||||||
ChatState {
|
ChatState {
|
||||||
always_on_top: Mutex::new(chat_conf.always_on_top),
|
stay_on_top: Mutex::new(chat_conf.stay_on_top),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||||
pub struct ChatConfJson {
|
pub struct ChatConfJson {
|
||||||
|
// support macOS only
|
||||||
pub titlebar: bool,
|
pub titlebar: bool,
|
||||||
pub always_on_top: bool,
|
pub hide_dock_icon: bool,
|
||||||
|
|
||||||
|
// macOS and Windows
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
|
|
||||||
|
pub stay_on_top: bool,
|
||||||
pub default_origin: String,
|
pub default_origin: String,
|
||||||
pub origin: String,
|
pub origin: String,
|
||||||
pub ua_pc: String,
|
pub ua_window: String,
|
||||||
pub ua_phone: String,
|
pub ua_tray: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatConfJson {
|
impl ChatConfJson {
|
||||||
@@ -60,15 +67,30 @@ impl ChatConfJson {
|
|||||||
/// path: ~/.chatgpt/chat.conf.json
|
/// path: ~/.chatgpt/chat.conf.json
|
||||||
pub fn init() -> PathBuf {
|
pub fn init() -> PathBuf {
|
||||||
let conf_file = ChatConfJson::conf_path();
|
let conf_file = ChatConfJson::conf_path();
|
||||||
|
let content = if cfg!(target_os = "macos") {
|
||||||
|
DEFAULT_CHAT_CONF_MAC
|
||||||
|
} else {
|
||||||
|
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();
|
||||||
#[cfg(target_os = "macos")]
|
return conf_file;
|
||||||
fs::write(&conf_file, DEFAULT_CHAT_CONF_MAC).unwrap();
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
fs::write(&conf_file, DEFAULT_CHAT_CONF).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
conf_file
|
conf_file
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,15 +99,31 @@ impl ChatConfJson {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_chat_conf() -> Self {
|
pub fn get_chat_conf() -> Self {
|
||||||
let config_file = fs::read_to_string(ChatConfJson::conf_path())
|
let conf_file = ChatConfJson::conf_path();
|
||||||
.unwrap_or_else(|_| DEFAULT_CHAT_CONF.to_string());
|
let file_content = fs::read_to_string(&conf_file).unwrap();
|
||||||
let config: Value =
|
let content = if cfg!(target_os = "macos") {
|
||||||
serde_json::from_str(&config_file).expect("failed to parse chat.conf.json");
|
DEFAULT_CHAT_CONF_MAC
|
||||||
serde_json::from_value(config).unwrap_or_else(|_| ChatConfJson::chat_conf_default())
|
} 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3
|
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3
|
||||||
pub fn amend(new_rules: &Value) -> Result<()> {
|
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> {
|
||||||
let config = ChatConfJson::get_chat_conf();
|
let config = ChatConfJson::get_chat_conf();
|
||||||
let config: Value = serde_json::to_value(&config)?;
|
let config: Value = serde_json::to_value(&config)?;
|
||||||
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
|
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
|
||||||
@@ -99,6 +137,20 @@ impl ChatConfJson {
|
|||||||
ChatConfJson::conf_path(),
|
ChatConfJson::conf_path(),
|
||||||
serde_json::to_string_pretty(&config)?,
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +172,4 @@ impl ChatConfJson {
|
|||||||
TitleBarStyle::Overlay
|
TitleBarStyle::Overlay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chat_conf_default() -> Self {
|
|
||||||
serde_json::from_value(serde_json::json!(DEFAULT_CHAT_CONF)).unwrap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,47 @@ mod app;
|
|||||||
mod conf;
|
mod conf;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use app::{cmd, menu, setup};
|
use app::{cmd, fs_extra, menu, setup};
|
||||||
use conf::{ChatConfJson, ChatState};
|
use conf::{ChatConfJson, ChatState};
|
||||||
|
use tauri::api::path;
|
||||||
|
use tauri_plugin_log::{
|
||||||
|
fern::colors::{Color, ColoredLevelConfig},
|
||||||
|
LogTarget, LoggerBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
ChatConfJson::init();
|
ChatConfJson::init();
|
||||||
|
// If the file does not exist, creating the file will block menu synchronization
|
||||||
|
utils::create_chatgpt_prompts();
|
||||||
let chat_conf = ChatConfJson::get_chat_conf();
|
let chat_conf = ChatConfJson::get_chat_conf();
|
||||||
let context = tauri::generate_context!();
|
let context = tauri::generate_context!();
|
||||||
|
let colors = ColoredLevelConfig {
|
||||||
|
error: Color::Red,
|
||||||
|
warn: Color::Yellow,
|
||||||
|
debug: Color::Blue,
|
||||||
|
info: Color::BrightGreen,
|
||||||
|
trace: Color::Cyan,
|
||||||
|
};
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
// https://github.com/tauri-apps/tauri/pull/2736
|
||||||
|
.plugin(
|
||||||
|
LoggerBuilder::new()
|
||||||
|
.level(if cfg!(debug_assertions) {
|
||||||
|
log::LevelFilter::Debug
|
||||||
|
} else {
|
||||||
|
log::LevelFilter::Trace
|
||||||
|
})
|
||||||
|
.with_colors(colors)
|
||||||
|
.targets([
|
||||||
|
// LogTarget::LogDir,
|
||||||
|
// LOG PATH: ~/.chatgpt/ChatGPT.log
|
||||||
|
LogTarget::Folder(path::home_dir().unwrap().join(".chatgpt")),
|
||||||
|
LogTarget::Stdout,
|
||||||
|
LogTarget::Webview,
|
||||||
|
])
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
.manage(ChatState::default(chat_conf))
|
.manage(ChatState::default(chat_conf))
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
cmd::drag_window,
|
cmd::drag_window,
|
||||||
@@ -26,20 +58,31 @@ fn main() {
|
|||||||
cmd::form_cancel,
|
cmd::form_cancel,
|
||||||
cmd::form_confirm,
|
cmd::form_confirm,
|
||||||
cmd::form_msg,
|
cmd::form_msg,
|
||||||
|
cmd::open_file,
|
||||||
|
cmd::get_chat_model_cmd,
|
||||||
|
cmd::parse_prompt,
|
||||||
|
cmd::window_reload,
|
||||||
|
cmd::cmd_list,
|
||||||
|
fs_extra::metadata,
|
||||||
])
|
])
|
||||||
.setup(setup::init)
|
.setup(setup::init)
|
||||||
.plugin(tauri_plugin_positioner::init())
|
.plugin(tauri_plugin_positioner::init())
|
||||||
.menu(menu::init(&context))
|
.menu(menu::init())
|
||||||
.system_tray(menu::tray_menu())
|
.system_tray(menu::tray_menu())
|
||||||
.on_menu_event(menu::menu_handler)
|
.on_menu_event(menu::menu_handler)
|
||||||
.on_system_tray_event(menu::tray_handler)
|
.on_system_tray_event(menu::tray_handler)
|
||||||
.on_window_event(|event| {
|
.on_window_event(|event| {
|
||||||
// https://github.com/tauri-apps/tauri/discussions/2684
|
// https://github.com/tauri-apps/tauri/discussions/2684
|
||||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
|
||||||
// TODO: https://github.com/tauri-apps/tauri/issues/3084
|
let win = event.window();
|
||||||
// event.window().hide().unwrap();
|
if win.label() == "main" {
|
||||||
// https://github.com/tauri-apps/tao/pull/517
|
win.hide().unwrap();
|
||||||
event.window().minimize().unwrap();
|
} else {
|
||||||
|
// TODO: https://github.com/tauri-apps/tauri/issues/3084
|
||||||
|
// event.window().hide().unwrap();
|
||||||
|
// https://github.com/tauri-apps/tao/pull/517
|
||||||
|
event.window().minimize().unwrap();
|
||||||
|
}
|
||||||
api.prevent_close();
|
api.prevent_close();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use log::info;
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
@@ -29,6 +30,14 @@ pub fn create_file(path: &Path) -> Result<File> {
|
|||||||
File::create(path).map_err(Into::into)
|
File::create(path).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn create_chatgpt_prompts() {
|
||||||
|
let sync_file = chat_root().join("cache_model").join("chatgpt_prompts.json");
|
||||||
|
if !exists(&sync_file) {
|
||||||
|
create_file(&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) {
|
||||||
@@ -48,6 +57,7 @@ pub fn user_script() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_file(path: PathBuf) {
|
pub fn open_file(path: PathBuf) {
|
||||||
|
info!("open_file: {}", path.to_string_lossy());
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
Command::new("open").arg("-R").arg(path).spawn().unwrap();
|
Command::new("open").arg("-R").arg(path).spawn().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,35 @@
|
|||||||
{
|
{
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "",
|
"beforeDevCommand": "npm run dev:fe",
|
||||||
"beforeBuildCommand": "",
|
"beforeBuildCommand": "npm run build:fe",
|
||||||
"devPath": "../dist",
|
"devPath": "http://localhost:1420",
|
||||||
"distDir": "../dist"
|
"distDir": "../dist"
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "ChatGPT",
|
"productName": "ChatGPT",
|
||||||
"version": "0.2.1"
|
"version": "0.6.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"all": true
|
"all": true,
|
||||||
|
"http": {
|
||||||
|
"all": true,
|
||||||
|
"scope": [
|
||||||
|
"https://**",
|
||||||
|
"http://**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"all": true,
|
||||||
|
"scope": [
|
||||||
|
"$HOME/.chatgpt/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"systemTray": {
|
"systemTray": {
|
||||||
"iconPath": "icons/tray-icon.png",
|
"iconPath": "icons/tray-icon.png",
|
||||||
"iconAsTemplate": true
|
"iconAsTemplate": true,
|
||||||
|
"menuOnLeftClick": false
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
@@ -44,13 +58,13 @@
|
|||||||
"shortDescription": "ChatGPT",
|
"shortDescription": "ChatGPT",
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"windows": {
|
"windows": {
|
||||||
"webviewInstallMode": {
|
|
||||||
"silent": true,
|
|
||||||
"type": "downloadBootstrapper"
|
|
||||||
},
|
|
||||||
"certificateThumbprint": null,
|
"certificateThumbprint": null,
|
||||||
"digestAlgorithm": "sha256",
|
"digestAlgorithm": "sha256",
|
||||||
"timestampUrl": ""
|
"timestampUrl": "",
|
||||||
|
"webviewInstallMode": {
|
||||||
|
"silent": true,
|
||||||
|
"type": "embedBootstrapper"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
@@ -63,6 +77,18 @@
|
|||||||
"https://lencx.github.io/ChatGPT/install.json"
|
"https://lencx.github.io/ChatGPT/install.json"
|
||||||
],
|
],
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIxMjY4OUI5MTVFNjBEMDUKUldRRkRlWVZ1WWttc1NGWEE0RFNSb0RqdnhsekRJZTkwK2hVLzhBZTZnaHExSEZ1ZEdzWkpXTHkK"
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIxMjY4OUI5MTVFNjBEMDUKUldRRkRlWVZ1WWttc1NGWEE0RFNSb0RqdnhsekRJZTkwK2hVLzhBZTZnaHExSEZ1ZEdzWkpXTHkK"
|
||||||
}
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"url": "index.html",
|
||||||
|
"title": "ChatGPT",
|
||||||
|
"visible": false,
|
||||||
|
"width": 800,
|
||||||
|
"height": 600,
|
||||||
|
"minWidth": 800,
|
||||||
|
"minHeight": 600
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
98
src/components/Tags/index.tsx
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { FC, useEffect, useRef, useState } from 'react';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Input, Tag } from 'antd';
|
||||||
|
import type { InputRef } from 'antd';
|
||||||
|
|
||||||
|
import { DISABLE_AUTO_COMPLETE } from '@/utils';
|
||||||
|
|
||||||
|
interface TagsProps {
|
||||||
|
value?: string[];
|
||||||
|
onChange?: (v: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tags: FC<TagsProps> = ({ value = [], onChange }) => {
|
||||||
|
const [tags, setTags] = useState<string[]>(value);
|
||||||
|
const [inputVisible, setInputVisible] = useState<boolean>(false);
|
||||||
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTags(value);
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inputVisible) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [inputVisible]);
|
||||||
|
|
||||||
|
const handleClose = (removedTag: string) => {
|
||||||
|
const newTags = tags.filter((tag) => tag !== removedTag);
|
||||||
|
setTags(newTags);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showInput = () => {
|
||||||
|
setInputVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputConfirm = () => {
|
||||||
|
if (inputValue && tags.indexOf(inputValue) === -1) {
|
||||||
|
const val = [...tags, inputValue];
|
||||||
|
setTags(val);
|
||||||
|
onChange && onChange(val);
|
||||||
|
}
|
||||||
|
setInputVisible(false);
|
||||||
|
setInputValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const forMap = (tag: string) => {
|
||||||
|
const tagElem = (
|
||||||
|
<Tag
|
||||||
|
closable
|
||||||
|
onClose={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleClose(tag);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span key={tag} style={{ display: 'inline-block' }}>
|
||||||
|
{tagElem}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagChild = tags.map(forMap);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span style={{ marginBottom: 16 }}>{tagChild}</span>
|
||||||
|
{inputVisible && (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
style={{ width: 78 }}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleInputConfirm}
|
||||||
|
onPressEnter={handleInputConfirm}
|
||||||
|
{...DISABLE_AUTO_COMPLETE}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!inputVisible && (
|
||||||
|
<Tag onClick={showInput} className="chat-tag-new">
|
||||||
|
<PlusOutlined /> New Tag
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tags;
|
||||||
53
src/hooks/useChatModel.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { clone } from 'lodash';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
|
||||||
|
import { CHAT_MODEL_JSON, CHAT_MODEL_CMD_JSON, readJSON, writeJSON } from '@/utils';
|
||||||
|
import useInit from '@/hooks/useInit';
|
||||||
|
|
||||||
|
export default function useChatModel(key: string, file = CHAT_MODEL_JSON) {
|
||||||
|
const [modelJson, setModelJson] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
|
useInit(async () => {
|
||||||
|
const data = await readJSON(file, {
|
||||||
|
defaultVal: { name: 'ChatGPT Model', [key]: null },
|
||||||
|
});
|
||||||
|
setModelJson(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelSet = async (data: Record<string, any>[]|Record<string, any>) => {
|
||||||
|
const oData = clone(modelJson);
|
||||||
|
oData[key] = data;
|
||||||
|
await writeJSON(file, oData);
|
||||||
|
setModelJson(oData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modelJson, modelSet, modelData: modelJson?.[key] || [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCacheModel(file = '') {
|
||||||
|
const [modelCacheJson, setModelCacheJson] = useState<Record<string, any>[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!file) return;
|
||||||
|
(async () => {
|
||||||
|
const data = await readJSON(file, { isRoot: true, isList: true });
|
||||||
|
setModelCacheJson(data);
|
||||||
|
})();
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
const modelCacheSet = async (data: Record<string, any>[], newFile = '') => {
|
||||||
|
await writeJSON(newFile ? newFile : file, data, { isRoot: true });
|
||||||
|
setModelCacheJson(data);
|
||||||
|
await modelCacheCmd();
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelCacheCmd = async () => {
|
||||||
|
// Generate the `chat.model.cmd.json` file and refresh the page for the slash command to take effect.
|
||||||
|
const list = await invoke('cmd_list');
|
||||||
|
await writeJSON(CHAT_MODEL_CMD_JSON, { name: 'ChatGPT CMD', last_updated: Date.now(), data: list });
|
||||||
|
await invoke('window_reload', { label: 'core' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return { modelCacheJson, modelCacheSet, modelCacheCmd };
|
||||||
|
}
|
||||||
44
src/hooks/useColumns.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
57
src/hooks/useData.ts
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const safeKey = Symbol('chat-id');
|
||||||
|
|
||||||
|
export default function useData(oData: any[]) {
|
||||||
|
const [opData, setData] = useState<any[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
opInit(oData);
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const opAdd = (val: any) => {
|
||||||
|
const v = [val, ...opData];
|
||||||
|
setData(v);
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
|
||||||
|
const opInit = (val: any[] = []) => {
|
||||||
|
if (!val || !Array.isArray(val)) return;
|
||||||
|
console.log('«20» /src/hooks/useData.ts ~> ', val);
|
||||||
|
|
||||||
|
const nData = val.map(i => ({ [safeKey]: v4(), ...i }));
|
||||||
|
setData(nData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const opRemove = (id: string) => {
|
||||||
|
const nData = opData.filter(i => i[safeKey] !== id);
|
||||||
|
setData(nData);
|
||||||
|
return nData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const opReplace = (id: string, data: any) => {
|
||||||
|
const nData = [...opData];
|
||||||
|
const idx = opData.findIndex(v => v[safeKey] === id);
|
||||||
|
nData[idx] = data;
|
||||||
|
setData(nData);
|
||||||
|
return nData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const opReplaceItems = (ids: string[], data: any) => {
|
||||||
|
const nData = [...opData];
|
||||||
|
let count = 0;
|
||||||
|
for (let i = 0; i < nData.length; i++) {
|
||||||
|
const v = nData[i];
|
||||||
|
if (ids.includes(v[safeKey])) {
|
||||||
|
count++;
|
||||||
|
nData[i] = { ...v, ...data };
|
||||||
|
}
|
||||||
|
if (count === ids.length) break;
|
||||||
|
}
|
||||||
|
setData(nData);
|
||||||
|
return nData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { opSafeKey: safeKey, opInit, opReplace, opAdd, opRemove, opData, opReplaceItems };
|
||||||
|
}
|
||||||
34
src/hooks/useEvent.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { invoke, path, http, fs, dialog } from '@tauri-apps/api';
|
||||||
|
|
||||||
|
import useInit from '@/hooks/useInit';
|
||||||
|
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
|
||||||
|
import { GITHUB_PROMPTS_CSV_URL, chatRoot, genCmd } from '@/utils';
|
||||||
|
|
||||||
|
export default function useEvent() {
|
||||||
|
const { modelSet } = useChatModel('sync_prompts');
|
||||||
|
const { modelCacheSet } = useCacheModel();
|
||||||
|
// Using `emit` and `listen` will be triggered multiple times in development mode.
|
||||||
|
// So here we use `eval` to call `__sync_prompt`
|
||||||
|
useInit(() => {
|
||||||
|
(window as any).__sync_prompts = async () => {
|
||||||
|
const res = await http.fetch(GITHUB_PROMPTS_CSV_URL, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: http.ResponseType.Text,
|
||||||
|
});
|
||||||
|
const data = (res.data || '') as string;
|
||||||
|
if (res.ok) {
|
||||||
|
const file = await path.join(await chatRoot(), 'cache_model', 'chatgpt_prompts.json');
|
||||||
|
const list: Record<string, string>[] = await invoke('parse_prompt', { data });
|
||||||
|
const fmtList = list.map(i => ({ ...i, cmd: i.cmd ? i.cmd : genCmd(i.act), enable: true, tags: ['chatgpt-prompts'] }));
|
||||||
|
await modelCacheSet(fmtList, file);
|
||||||
|
modelSet({
|
||||||
|
id: 'chatgpt_prompts',
|
||||||
|
last_updated: Date.now(),
|
||||||
|
});
|
||||||
|
dialog.message('ChatGPT Prompts data has been synchronized!');
|
||||||
|
} else {
|
||||||
|
dialog.message('ChatGPT Prompts data sync failed, please try again!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
12
src/hooks/useInit.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
// fix: Two interface requests will be made in development mode
|
||||||
|
export default function useInit(callback: () => void) {
|
||||||
|
const isInit = useRef(true);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInit.current) {
|
||||||
|
callback();
|
||||||
|
isInit.current = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
37
src/hooks/useTable.tsx
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Table } from 'antd';
|
||||||
|
import type { TableRowSelection } from 'antd/es/table/interface';
|
||||||
|
|
||||||
|
import { safeKey } from '@/hooks/useData';
|
||||||
|
|
||||||
|
export default function useTableRowSelection() {
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
const [selectedRowIDs, setSelectedRowIDs] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const onSelectChange = (newSelectedRowKeys: React.Key[], selectedRows: Record<string|symbol, any>) => {
|
||||||
|
const keys = selectedRows.map((i: any) => i[safeKey]);
|
||||||
|
setSelectedRowIDs(keys);
|
||||||
|
setSelectedRowKeys(newSelectedRowKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowSelection: TableRowSelection<Record<string, any>> = {
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: onSelectChange,
|
||||||
|
selections: [
|
||||||
|
Table.SELECTION_ALL,
|
||||||
|
Table.SELECTION_INVERT,
|
||||||
|
Table.SELECTION_NONE,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return { rowSelection, selectedRowIDs };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TABLE_PAGINATION = {
|
||||||
|
hideOnSinglePage: true,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
defaultPageSize: 5,
|
||||||
|
pageSizeOptions: [5, 10, 15, 20],
|
||||||
|
showTotal: (total: number) => <span>Total {total} items</span>,
|
||||||
|
};
|
||||||
32
src/layout/index.scss
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
.chat-logo {
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-sider-trigger {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-menu {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
.ant-menu-item {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-footer {
|
||||||
|
color: #666 !important;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
62
src/layout/index.tsx
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { FC, useState } from 'react';
|
||||||
|
import { Layout, Menu } from 'antd';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Routes, { menuItems } from '@/routes';
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
const { Content, Footer, Sider } = Layout;
|
||||||
|
|
||||||
|
interface ChatLayoutProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatLayout: FC<ChatLayoutProps> = ({ children }) => {
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
|
const go = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: '100vh' }} hasSider>
|
||||||
|
<Sider
|
||||||
|
theme="light"
|
||||||
|
collapsible
|
||||||
|
collapsed={collapsed}
|
||||||
|
onCollapse={(value) => setCollapsed(value)}
|
||||||
|
style={{
|
||||||
|
overflow: 'auto',
|
||||||
|
height: '100vh',
|
||||||
|
position: 'fixed',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="chat-logo"><img src="/logo.png" /></div>
|
||||||
|
<Menu
|
||||||
|
defaultSelectedKeys={[location.pathname]}
|
||||||
|
mode="inline"
|
||||||
|
inlineIndent={12}
|
||||||
|
items={menuItems}
|
||||||
|
defaultOpenKeys={['/model']}
|
||||||
|
onClick={(i) => go(i.key)}
|
||||||
|
/>
|
||||||
|
</Sider>
|
||||||
|
<Layout className="chat-layout" style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}>
|
||||||
|
<Content
|
||||||
|
className="chat-container"
|
||||||
|
style={{
|
||||||
|
overflow: 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Routes />
|
||||||
|
</Content>
|
||||||
|
<Footer style={{ textAlign: 'center' }}>
|
||||||
|
<a href="https://github.com/lencx/chatgpt" target="_blank">ChatGPT Desktop Application</a> ©2022 Created by lencx</Footer>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatLayout;
|
||||||
66
src/main.scss
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color: #2a2a2a;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-selection-column {
|
||||||
|
width: 50px !important;
|
||||||
|
min-width: 50px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-prompts-val {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-add-btn {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-prompts-tags {
|
||||||
|
.ant-tag {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-sync-path {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
// background-color: #d8d8d8;
|
||||||
|
color: #4096ff;
|
||||||
|
padding: 0 8px;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main.tsx
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { StrictMode, Suspense } from 'react';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
import useEvent from '@/hooks/useEvent';
|
||||||
|
import Layout from '@/layout';
|
||||||
|
import './main.scss';
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
useEvent();
|
||||||
|
return (
|
||||||
|
<BrowserRouter>
|
||||||
|
<Layout/>
|
||||||
|
</BrowserRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<App />
|
||||||
|
</Suspense>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
92
src/routes.tsx
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { useRoutes } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
DesktopOutlined,
|
||||||
|
BulbOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
FileSyncOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import type { MenuProps } from 'antd';
|
||||||
|
|
||||||
|
import General from '@view/General';
|
||||||
|
import UserCustom from '@/view/model/UserCustom';
|
||||||
|
import SyncPrompts from '@/view/model/SyncPrompts';
|
||||||
|
import SyncCustom from '@/view/model/SyncCustom';
|
||||||
|
import SyncRecord from '@/view/model/SyncRecord';
|
||||||
|
|
||||||
|
export type ChatRouteMetaObject = {
|
||||||
|
label: string;
|
||||||
|
icon?: React.ReactNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChatRouteObject = {
|
||||||
|
path: string;
|
||||||
|
element?: JSX.Element;
|
||||||
|
hideMenu?: boolean;
|
||||||
|
meta?: ChatRouteMetaObject;
|
||||||
|
children?: ChatRouteObject[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routes: Array<ChatRouteObject> = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <General />,
|
||||||
|
meta: {
|
||||||
|
label: 'General',
|
||||||
|
icon: <DesktopOutlined />,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/model',
|
||||||
|
meta: {
|
||||||
|
label: 'Language Model',
|
||||||
|
icon: <BulbOutlined />,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'user-custom',
|
||||||
|
element: <UserCustom />,
|
||||||
|
meta: {
|
||||||
|
label: 'User Custom',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sync-prompts',
|
||||||
|
element: <SyncPrompts />,
|
||||||
|
meta: {
|
||||||
|
label: 'Sync Prompts',
|
||||||
|
icon: <SyncOutlined />,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sync-custom',
|
||||||
|
element: <SyncCustom />,
|
||||||
|
meta: {
|
||||||
|
label: 'Sync Custom',
|
||||||
|
icon: <FileSyncOutlined />,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'sync-custom/:id',
|
||||||
|
element: <SyncRecord />,
|
||||||
|
hideMenu: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type MenuItem = Required<MenuProps>['items'][number];
|
||||||
|
export const menuItems: MenuItem[] = routes
|
||||||
|
.filter((j) => !j.hideMenu)
|
||||||
|
.map(i => ({
|
||||||
|
...i.meta,
|
||||||
|
key: i.path || '',
|
||||||
|
children: i?.children
|
||||||
|
?.filter((j) => !j.hideMenu)
|
||||||
|
?.map((j) => ({ ...j.meta, key: `${i.path}/${j.path}` || ''})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return useRoutes(routes);
|
||||||
|
};
|
||||||
68
src/utils.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { readTextFile, writeTextFile, exists, createDir, BaseDirectory } from '@tauri-apps/api/fs';
|
||||||
|
import { homeDir, join, dirname } from '@tauri-apps/api/path';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export const CHAT_MODEL_JSON = 'chat.model.json';
|
||||||
|
export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json';
|
||||||
|
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 DISABLE_AUTO_COMPLETE = {
|
||||||
|
autoCapitalize: 'off',
|
||||||
|
autoComplete: 'off',
|
||||||
|
spellCheck: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export const chatRoot = async () => {
|
||||||
|
return join(await homeDir(), '.chatgpt')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatModelPath = async (): Promise<string> => {
|
||||||
|
return join(await chatRoot(), CHAT_MODEL_JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const chatModelSyncPath = async (): Promise<string> => {
|
||||||
|
// return join(await chatRoot(), CHAT_MODEL_SYNC_JSON);
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const chatPromptsPath = async (): Promise<string> => {
|
||||||
|
return join(await chatRoot(), CHAT_PROMPTS_CSV);
|
||||||
|
}
|
||||||
|
|
||||||
|
type readJSONOpts = { defaultVal?: Record<string, any>, isRoot?: boolean, isList?: boolean };
|
||||||
|
export const readJSON = async (path: string, opts: readJSONOpts = {}) => {
|
||||||
|
const { defaultVal = {}, isRoot = false, isList = false } = opts;
|
||||||
|
const root = await chatRoot();
|
||||||
|
const file = await join(isRoot ? '' : root, path);
|
||||||
|
|
||||||
|
if (!await exists(file)) {
|
||||||
|
await createDir(await dirname(file), { recursive: true });
|
||||||
|
await writeTextFile(file, isList ? '[]' : JSON.stringify({
|
||||||
|
name: 'ChatGPT',
|
||||||
|
link: 'https://github.com/lencx/ChatGPT',
|
||||||
|
...defaultVal,
|
||||||
|
}, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(await readTextFile(file));
|
||||||
|
} catch(e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeJSONOpts = { dir?: string, isRoot?: boolean };
|
||||||
|
export const writeJSON = async (path: string, data: Record<string, any>, opts: writeJSONOpts = {}) => {
|
||||||
|
const { isRoot = false, dir = '' } = opts;
|
||||||
|
const root = await chatRoot();
|
||||||
|
const file = await join(isRoot ? '' : root, path);
|
||||||
|
|
||||||
|
if (isRoot && !await exists(await dirname(file))) {
|
||||||
|
await createDir(await join('.chatgpt', dir), { dir: BaseDirectory.Home });
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeTextFile(file, JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
98
src/view/General.tsx
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Form, Radio, Switch, Input, Button, Space, message, Tooltip } from 'antd';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { invoke } 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 { DISABLE_AUTO_COMPLETE } from '@/utils';
|
||||||
|
|
||||||
|
const OriginLabel = ({ url }: { url: string }) => {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
Switch Origin <Tooltip title={`Default: ${url}`}><QuestionCircleOutlined /></Tooltip>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function General() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [platformInfo, setPlatform] = useState<string>('');
|
||||||
|
const [chatConf, setChatConf] = useState<any>(null);
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
setPlatform(await platform());
|
||||||
|
const chatData = await invoke('get_chat_conf');
|
||||||
|
setChatConf(chatData);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
init();
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setFieldsValue(clone(chatConf));
|
||||||
|
}, [chatConf])
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
form.setFieldsValue(chatConf);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
style={{ maxWidth: 500 }}
|
||||||
|
onFinish={onFinish}
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 15, offset: 1 }}
|
||||||
|
>
|
||||||
|
<Form.Item label="Theme" name="theme">
|
||||||
|
<Radio.Group>
|
||||||
|
<Radio value="Light">Light</Radio>
|
||||||
|
<Radio value="Dark">Dark</Radio>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
<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={<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>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
105
src/view/model/SyncCustom/Form.tsx
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { useEffect, useState, ForwardRefRenderFunction, useImperativeHandle, forwardRef } from 'react';
|
||||||
|
import { Form, Input, Select, Tooltip } from 'antd';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import type { FormProps } from 'antd';
|
||||||
|
|
||||||
|
import { DISABLE_AUTO_COMPLETE, chatRoot } from '@/utils';
|
||||||
|
import useInit from '@/hooks/useInit';
|
||||||
|
|
||||||
|
interface SyncFormProps {
|
||||||
|
record?: Record<string|symbol, any> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initFormValue = {
|
||||||
|
act: '',
|
||||||
|
enable: true,
|
||||||
|
tags: [],
|
||||||
|
prompt: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record }, ref) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
useImperativeHandle(ref, () => ({ form }));
|
||||||
|
const [root, setRoot] = useState('');
|
||||||
|
|
||||||
|
useInit(async () => {
|
||||||
|
setRoot(await chatRoot());
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (record) {
|
||||||
|
form.setFieldsValue(record);
|
||||||
|
}
|
||||||
|
}, [record]);
|
||||||
|
|
||||||
|
const pathOptions = (
|
||||||
|
<Form.Item noStyle name="protocol" initialValue="https">
|
||||||
|
<Select>
|
||||||
|
<Select.Option value="local">{root}</Select.Option>
|
||||||
|
<Select.Option value="http">http://</Select.Option>
|
||||||
|
<Select.Option value="https">https://</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
const extOptions = (
|
||||||
|
<Form.Item noStyle name="ext" initialValue="json">
|
||||||
|
<Select>
|
||||||
|
<Select.Option value="csv">.csv</Select.Option>
|
||||||
|
<Select.Option value="json">.json</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
|
||||||
|
const jsonTip = (
|
||||||
|
<Tooltip
|
||||||
|
title={<pre>{JSON.stringify([
|
||||||
|
{ cmd: '', act: '', prompt: '' },
|
||||||
|
{ cmd: '', act: '', prompt: '' },
|
||||||
|
], null, 2)}</pre>}
|
||||||
|
>
|
||||||
|
<a>JSON</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
const csvTip = (
|
||||||
|
<Tooltip
|
||||||
|
title={<pre>{`"cmd","act","prompt"
|
||||||
|
"cmd","act","prompt"
|
||||||
|
"cmd","act","prompt"
|
||||||
|
"cmd","act","prompt"`}</pre>}
|
||||||
|
>
|
||||||
|
<a>CSV</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
labelCol={{ span: 4 }}
|
||||||
|
initialValues={initFormValue}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Name"
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: 'Please input name!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Please input name" {...DISABLE_AUTO_COMPLETE} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="PATH"
|
||||||
|
name="path"
|
||||||
|
rules={[{ required: true, message: 'Please input path!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="YOUR_PATH" addonBefore={pathOptions} addonAfter={extOptions} {...DISABLE_AUTO_COMPLETE} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item style={{ display: 'none' }} name="id" initialValue={v4().replace(/-/g, '')}><input /></Form.Item>
|
||||||
|
</Form>
|
||||||
|
<div className="tip">
|
||||||
|
<p>The file supports only {csvTip} and {jsonTip} formats.</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(SyncForm);
|
||||||
81
src/view/model/SyncCustom/config.tsx
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Tag, Space, Popconfirm } from 'antd';
|
||||||
|
import { HistoryOutlined } from '@ant-design/icons';
|
||||||
|
import { shell, path } from '@tauri-apps/api';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import useInit from '@/hooks/useInit';
|
||||||
|
import { chatRoot, fmtDate } from '@/utils';
|
||||||
|
|
||||||
|
export const syncColumns = () => [
|
||||||
|
{
|
||||||
|
title: 'Name',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Protocol',
|
||||||
|
dataIndex: 'protocol',
|
||||||
|
key: 'protocol',
|
||||||
|
width: 80,
|
||||||
|
render: (v: string) => <Tag>{v}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'PATH',
|
||||||
|
dataIndex: 'path',
|
||||||
|
key: 'path',
|
||||||
|
width: 180,
|
||||||
|
render: (_: string, row: any) => <RenderPath row={row} />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Last updated',
|
||||||
|
dataIndex: 'last_updated',
|
||||||
|
key: 'last_updated',
|
||||||
|
width: 140,
|
||||||
|
render: (v: number) => (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<HistoryOutlined style={{ marginRight: 5, color: v ? '#52c41a' : '#ff4d4f' }} />
|
||||||
|
{ v ? fmtDate(v) : ''}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 150,
|
||||||
|
render: (_: any, row: any, actions: any) => {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
<a onClick={() => actions.setRecord(row, 'sync')}>Sync</a>
|
||||||
|
{row.last_updated && <Link to={`${row.id}`} state={row}>View</Link>}
|
||||||
|
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
|
||||||
|
<Popconfirm
|
||||||
|
title="Are you sure to delete this path?"
|
||||||
|
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) => {
|
||||||
|
if (!/^http/.test(row.protocol)) {
|
||||||
|
return await path.join(await chatRoot(), row.path) + `.${row.ext}`;
|
||||||
|
} else {
|
||||||
|
return `${row.protocol}://${row.path}.${row.ext}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/view/model/SyncCustom/index.tsx
vendored
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Table, Modal, Button, message } from 'antd';
|
||||||
|
import { invoke, http, path, fs } from '@tauri-apps/api';
|
||||||
|
|
||||||
|
import useData from '@/hooks/useData';
|
||||||
|
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
|
||||||
|
import useColumns from '@/hooks/useColumns';
|
||||||
|
import { TABLE_PAGINATION } from '@/hooks/useTable';
|
||||||
|
import { CHAT_MODEL_JSON, chatRoot, readJSON, genCmd } from '@/utils';
|
||||||
|
import { syncColumns, getPath } from './config';
|
||||||
|
import SyncForm from './Form';
|
||||||
|
|
||||||
|
const setTag = (data: Record<string, any>[]) => data.map((i) => ({ ...i, tags: ['user-sync'], enable: true }))
|
||||||
|
|
||||||
|
export default function SyncCustom() {
|
||||||
|
const [isVisible, setVisible] = useState(false);
|
||||||
|
const { modelData, modelSet } = useChatModel('sync_custom', CHAT_MODEL_JSON);
|
||||||
|
const { modelCacheCmd, modelCacheSet } = useCacheModel();
|
||||||
|
const { opData, opInit, opAdd, opRemove, opReplace, opSafeKey } = useData([]);
|
||||||
|
const { columns, ...opInfo } = useColumns(syncColumns());
|
||||||
|
const formRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
setVisible(false);
|
||||||
|
opInfo.resetRecord();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modelData.length <= 0) return;
|
||||||
|
opInit(modelData);
|
||||||
|
}, [modelData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!opInfo.opType) return;
|
||||||
|
if (opInfo.opType === 'sync') {
|
||||||
|
const filename = `${opInfo?.opRecord?.id}.json`;
|
||||||
|
handleSync(filename).then(() => {
|
||||||
|
const data = opReplace(opInfo?.opRecord?.[opSafeKey], { ...opInfo?.opRecord, last_updated: Date.now() });
|
||||||
|
modelSet(data);
|
||||||
|
opInfo.resetRecord();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (['edit', 'new'].includes(opInfo.opType)) {
|
||||||
|
setVisible(true);
|
||||||
|
}
|
||||||
|
if (['delete'].includes(opInfo.opType)) {
|
||||||
|
const data = opRemove(opInfo?.opRecord?.[opSafeKey]);
|
||||||
|
modelSet(data);
|
||||||
|
opInfo.resetRecord();
|
||||||
|
}
|
||||||
|
}, [opInfo.opType, formRef]);
|
||||||
|
|
||||||
|
const handleSync = async (filename: string) => {
|
||||||
|
const record = opInfo?.opRecord;
|
||||||
|
const isJson = /json$/.test(record?.ext);
|
||||||
|
const file = await path.join(await chatRoot(), 'cache_model', filename);
|
||||||
|
const filePath = await getPath(record);
|
||||||
|
|
||||||
|
// https or http
|
||||||
|
if (/^http/.test(record?.protocol)) {
|
||||||
|
const res = await http.fetch(filePath, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: isJson ? 1 : 2,
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
if (isJson) {
|
||||||
|
// parse json
|
||||||
|
await modelCacheSet(setTag(Array.isArray(res?.data) ? res?.data : []), file);
|
||||||
|
} else {
|
||||||
|
// parse csv
|
||||||
|
const list: Record<string, string>[] = await invoke('parse_prompt', { data: res?.data });
|
||||||
|
const fmtList = list.map(i => ({ ...i, cmd: i.cmd ? i.cmd : genCmd(i.act), enable: true, tags: ['user-sync'] }));
|
||||||
|
await modelCacheSet(fmtList, file);
|
||||||
|
}
|
||||||
|
await modelCacheCmd();
|
||||||
|
message.success('ChatGPT Prompts data has been synchronized!');
|
||||||
|
} else {
|
||||||
|
message.error('ChatGPT Prompts data sync failed, please try again!');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// local
|
||||||
|
if (isJson) {
|
||||||
|
// parse json
|
||||||
|
const data = await readJSON(filePath, { isRoot: true });
|
||||||
|
await modelCacheSet(setTag(Array.isArray(data) ? data : []), file);
|
||||||
|
} else {
|
||||||
|
// parse csv
|
||||||
|
const data = await fs.readTextFile(filePath);
|
||||||
|
const list: Record<string, string>[] = await invoke('parse_prompt', { data });
|
||||||
|
const fmtList = list.map(i => ({ ...i, cmd: i.cmd ? i.cmd : genCmd(i.act), enable: true, tags: ['user-sync'] }));
|
||||||
|
await modelCacheSet(fmtList, file);
|
||||||
|
}
|
||||||
|
await modelCacheCmd();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
formRef.current?.form?.validateFields()
|
||||||
|
.then((vals: Record<string, any>) => {
|
||||||
|
let data = [];
|
||||||
|
switch (opInfo.opType) {
|
||||||
|
case 'new': data = opAdd(vals); break;
|
||||||
|
case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
modelSet(data);
|
||||||
|
hide();
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="chat-add-btn"
|
||||||
|
type="primary"
|
||||||
|
onClick={opInfo.opNew}
|
||||||
|
>
|
||||||
|
Add PATH
|
||||||
|
</Button>
|
||||||
|
<Table
|
||||||
|
key="id"
|
||||||
|
rowKey="name"
|
||||||
|
columns={columns}
|
||||||
|
scroll={{ x: 800 }}
|
||||||
|
dataSource={opData}
|
||||||
|
pagination={TABLE_PAGINATION}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
open={isVisible}
|
||||||
|
onCancel={hide}
|
||||||
|
title="Model PATH"
|
||||||
|
onOk={handleOk}
|
||||||
|
destroyOnClose
|
||||||
|
maskClosable={false}
|
||||||
|
>
|
||||||
|
<SyncForm ref={formRef} record={opInfo?.opRecord} />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/view/model/SyncPrompts/config.tsx
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Switch, Tag, Tooltip } from 'antd';
|
||||||
|
|
||||||
|
import { genCmd } from '@/utils';
|
||||||
|
|
||||||
|
export const syncColumns = () => [
|
||||||
|
{
|
||||||
|
title: '/{cmd}',
|
||||||
|
dataIndex: 'cmd',
|
||||||
|
fixed: 'left',
|
||||||
|
// width: 120,
|
||||||
|
key: 'cmd',
|
||||||
|
render: (_: string, row: Record<string, string>) => (
|
||||||
|
<Tag color="#2a2a2a">/{genCmd(row.act)}</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Act',
|
||||||
|
dataIndex: 'act',
|
||||||
|
key: 'act',
|
||||||
|
// width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tags',
|
||||||
|
dataIndex: 'tags',
|
||||||
|
key: 'tags',
|
||||||
|
// width: 150,
|
||||||
|
render: () => <Tag>chatgpt-prompts</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Enable',
|
||||||
|
dataIndex: 'enable',
|
||||||
|
key: 'enable',
|
||||||
|
// width: 80,
|
||||||
|
render: (v: boolean = false, row: Record<string, any>, action: Record<string, any>) => (
|
||||||
|
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Prompt',
|
||||||
|
dataIndex: 'prompt',
|
||||||
|
key: 'prompt',
|
||||||
|
// width: 300,
|
||||||
|
render: (v: string) => (
|
||||||
|
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
12
src/view/model/SyncPrompts/index.scss
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.chat-table-tip, .chat-table-btns {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-table-btns {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.num {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/view/model/SyncPrompts/index.tsx
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Table, Button, message, Popconfirm } from 'antd';
|
||||||
|
import { invoke, http, path, shell } from '@tauri-apps/api';
|
||||||
|
|
||||||
|
import useInit from '@/hooks/useInit';
|
||||||
|
import useData from '@/hooks/useData';
|
||||||
|
import useColumns from '@/hooks/useColumns';
|
||||||
|
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
|
||||||
|
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
|
||||||
|
import { fmtDate, chatRoot, GITHUB_PROMPTS_CSV_URL, genCmd } from '@/utils';
|
||||||
|
import { syncColumns } from './config';
|
||||||
|
import './index.scss';
|
||||||
|
|
||||||
|
const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv';
|
||||||
|
|
||||||
|
export default function SyncPrompts() {
|
||||||
|
const { rowSelection, selectedRowIDs } = useTable();
|
||||||
|
const [jsonPath, setJsonPath] = useState('');
|
||||||
|
const { modelJson, modelSet } = useChatModel('sync_prompts');
|
||||||
|
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
|
||||||
|
const { opData, opInit, opReplace, opReplaceItems, opSafeKey } = useData([]);
|
||||||
|
const { columns, ...opInfo } = useColumns(syncColumns());
|
||||||
|
const lastUpdated = modelJson?.sync_prompts?.last_updated;
|
||||||
|
const selectedItems = rowSelection.selectedRowKeys || [];
|
||||||
|
|
||||||
|
useInit(async () => {
|
||||||
|
setJsonPath(await path.join(await chatRoot(), 'cache_model', 'chatgpt_prompts.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modelCacheJson.length <= 0) return;
|
||||||
|
opInit(modelCacheJson);
|
||||||
|
}, [modelCacheJson.length]);
|
||||||
|
|
||||||
|
const handleSync = async () => {
|
||||||
|
const res = await http.fetch(GITHUB_PROMPTS_CSV_URL, {
|
||||||
|
method: 'GET',
|
||||||
|
responseType: http.ResponseType.Text,
|
||||||
|
});
|
||||||
|
const data = (res.data || '') as string;
|
||||||
|
if (res.ok) {
|
||||||
|
// const content = data.replace(/"(\s+)?,(\s+)?"/g, '","');
|
||||||
|
const list: Record<string, string>[] = await invoke('parse_prompt', { data });
|
||||||
|
const fmtList = list.map(i => ({ ...i, cmd: i.cmd ? i.cmd : genCmd(i.act), enable: true, tags: ['chatgpt-prompts'] }));
|
||||||
|
await modelCacheSet(fmtList);
|
||||||
|
opInit(fmtList);
|
||||||
|
modelSet({
|
||||||
|
id: 'chatgpt_prompts',
|
||||||
|
last_updated: Date.now(),
|
||||||
|
});
|
||||||
|
message.success('ChatGPT Prompts data has been synchronized!');
|
||||||
|
} else {
|
||||||
|
message.error('ChatGPT Prompts data sync failed, please try again!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opInfo.opType === 'enable') {
|
||||||
|
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
|
||||||
|
modelCacheSet(data);
|
||||||
|
}
|
||||||
|
}, [opInfo.opTime]);
|
||||||
|
|
||||||
|
const handleEnable = (isEnable: boolean) => {
|
||||||
|
const data = opReplaceItems(selectedRowIDs, { enable: isEnable })
|
||||||
|
modelCacheSet(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="chat-table-btns">
|
||||||
|
<div>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Button type="primary" onClick={() => handleEnable(true)}>Enable</Button>
|
||||||
|
<Button onClick={() => handleEnable(false)}>Disable</Button>
|
||||||
|
<span className="num">Selected {selectedItems.length} items</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Popconfirm
|
||||||
|
title={<span>Data sync will enable all prompts,<br/>are you sure you want to sync?</span>}
|
||||||
|
placement="topLeft"
|
||||||
|
onConfirm={handleSync}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<Button type="primary">Sync</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
<div className="chat-table-tip">
|
||||||
|
<div className="chat-sync-path">
|
||||||
|
<div>PATH: <a onClick={() => shell.open(promptsURL)} target="_blank" title={promptsURL}>f/awesome-chatgpt-prompts/prompts.csv</a></div>
|
||||||
|
<div>CACHE: <a onClick={() => shell.open(jsonPath)} target="_blank" title={jsonPath}>{jsonPath}</a></div>
|
||||||
|
</div>
|
||||||
|
{lastUpdated && <span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>Last updated on {fmtDate(lastUpdated)}</span>}
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
key={lastUpdated}
|
||||||
|
rowKey="act"
|
||||||
|
columns={columns}
|
||||||
|
scroll={{ x: 'auto' }}
|
||||||
|
dataSource={opData}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
pagination={TABLE_PAGINATION}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/view/model/SyncRecord/config.tsx
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Switch, Tag, Tooltip } from 'antd';
|
||||||
|
|
||||||
|
import { genCmd } from '@/utils';
|
||||||
|
|
||||||
|
export const syncColumns = () => [
|
||||||
|
{
|
||||||
|
title: '/{cmd}',
|
||||||
|
dataIndex: 'cmd',
|
||||||
|
fixed: 'left',
|
||||||
|
// width: 120,
|
||||||
|
key: 'cmd',
|
||||||
|
render: (_: string, row: Record<string, string>) => (
|
||||||
|
<Tag color="#2a2a2a">/{genCmd(row.act)}</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Act',
|
||||||
|
dataIndex: 'act',
|
||||||
|
key: 'act',
|
||||||
|
// width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tags',
|
||||||
|
dataIndex: 'tags',
|
||||||
|
key: 'tags',
|
||||||
|
// width: 150,
|
||||||
|
render: () => <Tag>chatgpt-prompts</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Enable',
|
||||||
|
dataIndex: 'enable',
|
||||||
|
key: 'enable',
|
||||||
|
// width: 80,
|
||||||
|
render: (v: boolean = false, row: Record<string, any>, action: Record<string, any>) => (
|
||||||
|
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Prompt',
|
||||||
|
dataIndex: 'prompt',
|
||||||
|
key: 'prompt',
|
||||||
|
// width: 300,
|
||||||
|
render: (v: string) => (
|
||||||
|
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
85
src/view/model/SyncRecord/index.tsx
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
|
import { Table, Button } from 'antd';
|
||||||
|
import { shell, path } from '@tauri-apps/api';
|
||||||
|
|
||||||
|
import useColumns from '@/hooks/useColumns';
|
||||||
|
import useData from '@/hooks/useData';
|
||||||
|
import { useCacheModel } from '@/hooks/useChatModel';
|
||||||
|
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
|
||||||
|
import { fmtDate, chatRoot } from '@/utils';
|
||||||
|
import { getPath } from '@/view/model/SyncCustom/config';
|
||||||
|
import { syncColumns } from './config';
|
||||||
|
import useInit from '@/hooks/useInit';
|
||||||
|
|
||||||
|
export default function SyncRecord() {
|
||||||
|
const location = useLocation();
|
||||||
|
const [filePath, setFilePath] = useState('');
|
||||||
|
const [jsonPath, setJsonPath] = useState('');
|
||||||
|
const state = location?.state;
|
||||||
|
|
||||||
|
const { rowSelection, selectedRowIDs } = useTable();
|
||||||
|
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
|
||||||
|
const { opData, opInit, opReplace, opReplaceItems, opSafeKey } = useData([]);
|
||||||
|
const { columns, ...opInfo } = useColumns(syncColumns());
|
||||||
|
|
||||||
|
const selectedItems = rowSelection.selectedRowKeys || [];
|
||||||
|
|
||||||
|
useInit(async () => {
|
||||||
|
setFilePath(await getPath(state));
|
||||||
|
setJsonPath(await path.join(await chatRoot(), 'cache_model', `${state?.id}.json`));
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modelCacheJson.length <= 0) return;
|
||||||
|
opInit(modelCacheJson);
|
||||||
|
}, [modelCacheJson.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opInfo.opType === 'enable') {
|
||||||
|
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
|
||||||
|
modelCacheSet(data);
|
||||||
|
}
|
||||||
|
}, [opInfo.opTime]);
|
||||||
|
|
||||||
|
const handleEnable = (isEnable: boolean) => {
|
||||||
|
const data = opReplaceItems(selectedRowIDs, { enable: isEnable })
|
||||||
|
modelCacheSet(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="chat-table-btns">
|
||||||
|
<div>
|
||||||
|
<Button shape="round" icon={<ArrowLeftOutlined />} onClick={() => history.back()} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Button type="primary" onClick={() => handleEnable(true)}>Enable</Button>
|
||||||
|
<Button onClick={() => handleEnable(false)}>Disable</Button>
|
||||||
|
<span className="num">Selected {selectedItems.length} items</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="chat-table-tip">
|
||||||
|
<div className="chat-sync-path">
|
||||||
|
<div>PATH: <a onClick={() => shell.open(filePath)} target="_blank" title={filePath}>{filePath}</a></div>
|
||||||
|
<div>CACHE: <a onClick={() => shell.open(jsonPath)} target="_blank" title={jsonPath}>{jsonPath}</a></div>
|
||||||
|
</div>
|
||||||
|
{state?.last_updated && <span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>Last updated on {fmtDate(state?.last_updated)}</span>}
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
key="prompt"
|
||||||
|
rowKey="act"
|
||||||
|
columns={columns}
|
||||||
|
scroll={{ x: 'auto' }}
|
||||||
|
dataSource={opData}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
pagination={TABLE_PAGINATION}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
66
src/view/model/UserCustom/Form.tsx
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
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 UserCustomFormProps {
|
||||||
|
record?: Record<string|symbol, any> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initFormValue = {
|
||||||
|
act: '',
|
||||||
|
enable: true,
|
||||||
|
tags: [],
|
||||||
|
prompt: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCustomForm: ForwardRefRenderFunction<FormProps, UserCustomFormProps> = ({ 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="/{cmd}"
|
||||||
|
name="cmd"
|
||||||
|
rules={[{ required: true, message: 'Please input {cmd}!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Please input {cmd}" {...DISABLE_AUTO_COMPLETE} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Act"
|
||||||
|
name="act"
|
||||||
|
rules={[{ required: true, message: 'Please input act!' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Please input act" {...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.Item
|
||||||
|
label="Prompt"
|
||||||
|
name="prompt"
|
||||||
|
rules={[{ required: true, message: 'Please input prompt!' }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={4} placeholder="Please input prompt" {...DISABLE_AUTO_COMPLETE} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(UserCustomForm);
|
||||||
64
src/view/model/UserCustom/config.tsx
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Tag, Switch, Tooltip, Space, Popconfirm } from 'antd';
|
||||||
|
|
||||||
|
export const modelColumns = () => [
|
||||||
|
{
|
||||||
|
title: '/{cmd}',
|
||||||
|
dataIndex: 'cmd',
|
||||||
|
fixed: 'left',
|
||||||
|
width: 120,
|
||||||
|
key: 'cmd',
|
||||||
|
render: (v: string) => <Tag color="#2a2a2a">/{v}</Tag>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Act',
|
||||||
|
dataIndex: 'act',
|
||||||
|
key: 'act',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tags',
|
||||||
|
dataIndex: 'tags',
|
||||||
|
key: 'tags',
|
||||||
|
width: 150,
|
||||||
|
render: (v: string[]) => (
|
||||||
|
<span className="chat-prompts-tags">{v?.map(i => <Tag key={i}>{i}</Tag>)}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Enable',
|
||||||
|
dataIndex: 'enable',
|
||||||
|
key: 'enable',
|
||||||
|
width: 80,
|
||||||
|
render: (v: boolean = false, row: Record<string, any>, action: Record<string, any>) => (
|
||||||
|
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Prompt',
|
||||||
|
dataIndex: 'prompt',
|
||||||
|
key: 'prompt',
|
||||||
|
width: 300,
|
||||||
|
render: (v: string) => (
|
||||||
|
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 120,
|
||||||
|
render: (_: any, row: any, actions: any) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
|
||||||
|
<Popconfirm
|
||||||
|
title="Are you sure to delete this model?"
|
||||||
|
onConfirm={() => actions.setRecord(row, 'delete')}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<a>Delete</a>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
];
|
||||||
139
src/view/model/UserCustom/index.tsx
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Table, Button, Modal, message } from 'antd';
|
||||||
|
import { shell, path } from '@tauri-apps/api';
|
||||||
|
|
||||||
|
import useInit from '@/hooks/useInit';
|
||||||
|
import useData from '@/hooks/useData';
|
||||||
|
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
|
||||||
|
import useColumns from '@/hooks/useColumns';
|
||||||
|
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
|
||||||
|
import { chatRoot, fmtDate } from '@/utils';
|
||||||
|
import { modelColumns } from './config';
|
||||||
|
import UserCustomForm from './Form';
|
||||||
|
|
||||||
|
export default function LanguageModel() {
|
||||||
|
const { rowSelection, selectedRowIDs } = useTable();
|
||||||
|
const [isVisible, setVisible] = useState(false);
|
||||||
|
const [jsonPath, setJsonPath] = useState('');
|
||||||
|
const { modelJson, modelSet } = useChatModel('user_custom');
|
||||||
|
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
|
||||||
|
const { opData, opInit, opReplaceItems, opAdd, opRemove, opReplace, opSafeKey } = useData([]);
|
||||||
|
const { columns, ...opInfo } = useColumns(modelColumns());
|
||||||
|
const lastUpdated = modelJson?.user_custom?.last_updated;
|
||||||
|
const selectedItems = rowSelection.selectedRowKeys || [];
|
||||||
|
const formRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useInit(async () => {
|
||||||
|
setJsonPath(await path.join(await chatRoot(), 'cache_model', 'user_custom.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modelCacheJson.length <= 0) return;
|
||||||
|
opInit(modelCacheJson);
|
||||||
|
}, [modelCacheJson.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]);
|
||||||
|
modelCacheSet(data);
|
||||||
|
opInfo.resetRecord();
|
||||||
|
}
|
||||||
|
}, [opInfo.opType, formRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opInfo.opType === 'enable') {
|
||||||
|
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
|
||||||
|
modelCacheSet(data);
|
||||||
|
}
|
||||||
|
}, [opInfo.opTime])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (opInfo.opType === 'enable') {
|
||||||
|
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
|
||||||
|
modelCacheSet(data);
|
||||||
|
}
|
||||||
|
}, [opInfo.opTime]);
|
||||||
|
|
||||||
|
const handleEnable = (isEnable: boolean) => {
|
||||||
|
const data = opReplaceItems(selectedRowIDs, { enable: isEnable })
|
||||||
|
modelCacheSet(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
setVisible(false);
|
||||||
|
opInfo.resetRecord();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
formRef.current?.form?.validateFields()
|
||||||
|
.then(async (vals: Record<string, any>) => {
|
||||||
|
if (modelCacheJson.map((i: any) => i.cmd).includes(vals.cmd) && opInfo?.opRecord?.cmd !== vals.cmd) {
|
||||||
|
message.warning(`"cmd: /${vals.cmd}" already exists, please change the "${vals.cmd}" name and resubmit.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let data = [];
|
||||||
|
switch (opInfo.opType) {
|
||||||
|
case 'new': data = opAdd(vals); break;
|
||||||
|
case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break;
|
||||||
|
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`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="chat-table-btns">
|
||||||
|
<Button className="chat-add-btn" type="primary" onClick={opInfo.opNew}>Add Model</Button>
|
||||||
|
<div>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Button type="primary" onClick={() => handleEnable(true)}>Enable</Button>
|
||||||
|
<Button onClick={() => handleEnable(false)}>Disable</Button>
|
||||||
|
<span className="num">Selected {selectedItems.length} items</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* <div className="chat-model-path">PATH: <span onClick={handleOpenFile}>{modelPath}</span></div> */}
|
||||||
|
<div className="chat-table-tip">
|
||||||
|
<div className="chat-sync-path">
|
||||||
|
<div>CACHE: <a onClick={() => shell.open(jsonPath)} title={jsonPath}>{jsonPath}</a></div>
|
||||||
|
</div>
|
||||||
|
{lastUpdated && <span style={{ marginLeft: 10, color: '#888', fontSize: 12 }}>Last updated on {fmtDate(lastUpdated)}</span>}
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
key={lastUpdated}
|
||||||
|
rowKey="cmd"
|
||||||
|
columns={columns}
|
||||||
|
scroll={{ x: 'auto' }}
|
||||||
|
dataSource={opData}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
pagination={TABLE_PAGINATION}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
open={isVisible}
|
||||||
|
onCancel={hide}
|
||||||
|
title={modalTitle}
|
||||||
|
onOk={handleOk}
|
||||||
|
destroyOnClose
|
||||||
|
maskClosable={false}
|
||||||
|
>
|
||||||
|
<UserCustomForm record={opInfo?.opRecord} ref={formRef} />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
28
tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@view/*": ["src/view/*"],
|
||||||
|
"@comps/*": ["src/components/*"],
|
||||||
|
"@layout/*": ["src/layout/*"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
9
tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
28
vite.config.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tsconfigPaths(), react()],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
// prevent vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
// to make use of `TAURI_DEBUG` and other env variables
|
||||||
|
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||||
|
envPrefix: ["VITE_", "TAURI_"],
|
||||||
|
build: {
|
||||||
|
// Tauri supports es2021
|
||||||
|
target: ["es2021", "chrome100", "safari13"],
|
||||||
|
// don't minify for debug builds
|
||||||
|
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
||||||
|
// produce sourcemaps for debug builds
|
||||||
|
sourcemap: !!process.env.TAURI_DEBUG,
|
||||||
|
},
|
||||||
|
});
|
||||||