Compare commits

..

5 Commits

Author SHA1 Message Date
lencx
1607261e8f v0.1.6 2022-12-10 03:47:50 +08:00
lencx
508875a944 chore: export 2022-12-10 03:42:41 +08:00
lencx
f585f58987 readme 2022-12-10 03:29:50 +08:00
lencx
0d897e0176 feat: export history 2022-12-10 03:23:29 +08:00
lencx
dc33c6eb4f feat: import 2022-12-09 13:44:18 +08:00
58 changed files with 203 additions and 2280 deletions

5
.gitattributes vendored
View File

@@ -1,4 +1 @@
*.js linguist-vendored
*.tsx linguist-vendored
*.scss linguist-vendored
src/**/*.ts linguist-vendored
*.js linguist-vendored

View File

@@ -75,7 +75,7 @@ jobs:
${{ runner.os }}-yarn-
- name: Install app dependencies and build it
run: yarn && yarn build:fe
run: yarn
- uses: tauri-apps/tauri-action@v0.3
env:

29
.gitignore vendored
View File

@@ -1,32 +1,9 @@
.DS_Store
*.lock
package-lock.json
node_modules/
yarn.lock
*.lock
# rust
target/
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?

View File

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

214
LICENSE
View File

@@ -1,201 +1,21 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
MIT License
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Copyright (c) 2022 lencx
1. Definitions.
Permission is hereby granted, free of charge, to any person obtaining a copy
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:
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"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.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,179 +0,0 @@
<p align="center">
<img width="180" src="./public/logo.png" alt="ChatGPT">
<h1 align="center">ChatGPT</h1>
</p>
> ChatGPT 桌面应用
[![English badge](https://img.shields.io/badge/%E8%8B%B1%E6%96%87-English-blue)](./README.md)
[![简体中文 badge](https://img.shields.io/badge/%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-Simplified%20Chinese-blue)](./README-ZH_CN.md)
[![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases)
[![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr)
[![lencx](https://img.shields.io/twitter/follow/lencx_.svg?style=social)](https://twitter.com/lencx_)
[Awesome ChatGPT](./AWESOME.md)
## 📦 下载
[📝 更新日志](./UPDATE_LOG.md)
<!-- download start -->
**最新版:**
- `Mac`: [ChatGPT_0.4.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.0_x64.dmg)
- `Linux`: [chat-gpt_0.4.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/chat-gpt_0.4.0_amd64.deb)
- `Windows`: [ChatGPT_0.4.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.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) 来寻找有趣的功能来导入到应用。
![chat cmd](./assets/chat-cmd-1.png)
![chat cmd](./assets/chat-cmd-2.png)
数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。
项目会维护一份常用命令,您也可以直接将 [chat.model.json](https://github.com/lencx/ChatGPT/blob/main/chat.model.json) 复制到你的本地目录 `~/.chatgpt/chat.model.json`
在 ChatGPT 文本输入区域,键入 `/` 开头的字符,则会弹出指令提示,按下空格键,它会默认将命令关联的文本填充到输入区域(注意:如果包含多个指令提示,它只会选择第一个作为填充,你可以持续输入,直到第一个提示命令为你想要时,再按下空格键。或者使用鼠标来点击多条指令中的某一个)。填充完成后,你只需要按下回车键即可。
![chatgpt](assets/chatgpt.gif)
## ✨ 功能概览
- 跨平台: `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`: 网站调试工具,调试页面或脚本可能需要
## 👀 预览
<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) 项目为这个应用自定义指令功能所带来的启发
---
[![Star History Chart](https://api.star-history.com/svg?repos=lencx/chatgpt&type=Date)](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

146
README.md
View File

@@ -1,130 +1,39 @@
<p align="center">
<img width="180" src="./public/logo.png" alt="ChatGPT">
<img width="180" src="./logo.png" alt="ChatGPT">
<h1 align="center">ChatGPT</h1>
</p>
> ChatGPT Desktop Application
[![English badge](https://img.shields.io/badge/%E8%8B%B1%E6%96%87-English-blue)](./README.md)
[![简体中文 badge](https://img.shields.io/badge/%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-Simplified%20Chinese-blue)](./README-ZH_CN.md)
## Downloads
[![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases)
[![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr)
[![lencx](https://img.shields.io/twitter/follow/lencx_.svg?style=social)](https://twitter.com/lencx_)
<!-- [![中文版 badge](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-Traditional%20Chinese-blue)](./README-ZH.md) -->
[Awesome ChatGPT](./AWESOME.md)
## 📦 Downloads
[📝 Update Log](./UPDATE_LOG.md)
<!-- download start -->
**Latest:**
- `Mac`: [ChatGPT_0.4.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.0_x64.dmg)
- `Linux`: [chat-gpt_0.4.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/chat-gpt_0.4.0_amd64.deb)
- `Windows`: [ChatGPT_0.4.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.0_x64_en-US.msi)
- `Mac`: [ChatGPT_0.1.6_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/ChatGPT_0.1.6_x64.dmg)
- `Linux`: [chat-gpt_0.1.6_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/chat-gpt_0.1.6_amd64.deb)
- `Windows`: [ChatGPT_0.1.6_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.1.5/ChatGPT_0.1.6_x64_en-US.msi)
[Other version...](https://github.com/lencx/ChatGPT/releases)
<!-- download end -->
## Features
### Install
- multi-platform: `macOS` `Linux` `Windows`
- export ChatGPT history (PNG, PDF and Share Link)
- always on top (whether the window should always be on top of other windows)
- inject script
- auto updater
- app menu
- system tray
- shortcut
Easily install with _[Homebrew](https://brew.sh) ([Cask](https://docs.brew.sh/Cask-Cookbook)):_
## Preview
~~~ sh
brew tap lencx/chatgpt https://github.com/lencx/ChatGPT.git
brew install --cask chatgpt --no-quarantine
~~~
<img width="360" src="./assets/install.png" alt="install"> <img width="360" src="./assets/chat.png" alt="chat">
<img width="360" src="./assets/export.png" alt="export"> <img width="360" src="./assets/auto-update.png" alt="auto update">
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 }
~~~
## 📢 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.
![chat cmd](./assets/chat-cmd-1.png)
![chat cmd](./assets/chat-cmd-2.png)
After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`).
The project maintains a list of common commands, or you can copy [chat.model.json](https://github.com/lencx/ChatGPT/blob/main/chat.model.json) directly to your local directory `~/.chatgpt/chat.model.json`
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.
![chatgpt](assets/chatgpt.gif)
## ✨ Features
- Multi-platform: `macOS` `Linux` `Windows`
- Export ChatGPT history (PNG, PDF and Share Link)
- Automatic application upgrade notification
- Common shortcut keys
- System tray hover window
- Powerful menu items
### MenuItem
- **Preferences**
- `Theme` - `Light`, `Dark` (Only macOS and Windows are supported).
- `Stay On Top`: The window is stay on top of other windows.
- `Titlebar`: Whether to display the titlebar, supported by macOS only.
- `Hide Dock Icon` ([#35](https://github.com/lencx/ChatGPT/issues/35)): Hide application icons from the Dock(support macOS only).
- 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`).
- `Inject Script`: Using scripts to modify pages.
- `Control Center`: The control center of ChatGPT application, it will give unlimited imagination to the application.
- `Theme`, `Stay On Top`, `Titlebar`, ...
- `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`, ...
- **View** - `Go Back`, `Go Forward`, `Scroll to Top of Screen`, `Scroll to Bottom of Screen`, `Refresh the Screen`, ...
- **Help**
- `Update Log`: ChatGPT changelog.
- `Report Bug`: Report a bug or give feedback.
- `Toggle Developer Tools`: Developer debugging tools.
## TODO
- Web access capability ([#20](https://github.com/lencx/ChatGPT/issues/20))
- Shortcut command typing chatgpt prompt
- ...
## 👀 Preview
<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>
## ❓FAQ
### Can't open ChatGPT
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`).
## FAQ
### Is it safe?
@@ -163,21 +72,6 @@ yarn dev
yarn build
```
## ❤️ Thanks
## Related
- 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.
---
[![Star History Chart](https://api.star-history.com/svg?repos=lencx/chatgpt&type=Date)](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
- [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

View File

@@ -1,48 +1,9 @@
# UPDATE LOG
## 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
feat: menu optimization
## v0.2.0
feat: menu enhancement
- customize user-agent to prevent security detection interception
- clear all chatgpt configuration files
## v0.1.8
feat:
- menu enhancement: theme, titlebar
- modify website address
## v0.1.7
feat: tray window
## v0.1.6
feat:
- stay on top
- always on top
- export ChatGPT history
## v0.1.5

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 KiB

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 989 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 KiB

View File

@@ -1,22 +0,0 @@
cask "chatgpt" do
version "0.1.7"
sha256 "1320b30a67e2506f9b45ffd2a48243d6141171c231dd698994ae5156a637eb3f"
url "https://github.com/lencx/ChatGPT/releases/download/v#{version}/ChatGPT_#{version}_x64.dmg"
name "ChatGPT"
desc "Desktop wrapper for OpenAI ChatGPT"
homepage "https://github.com/lencx/ChatGPT#readme"
app "ChatGPT.app"
uninstall quit: "com.lencx.chatgpt"
zap trash: [
"~/.chatgpt",
"~/Library/Caches/com.lencx.chatgpt",
"~/Library/HTTPStorages/com.lencx.chatgpt.binarycookies",
"~/Library/Preferences/com.lencx.chatgpt.plist",
"~/Library/Saved Application State/com.lencx.chatgpt.savedState",
"~/Library/WebKit/com.lencx.chatgpt",
]
end

View File

@@ -1,82 +0,0 @@
{
"name": "ChatGPT Model",
"link": "https://github.com/lencx/ChatGPT/blob/main/chat.model.md",
"data": [
{
"cmd": "cfm",
"act": "Character from Movie/Book/Anything",
"tags": [],
"enable": true,
"prompt": "I want you to act like {Character} from {series}. I want you to respond and answer like {Character}. do not write any explanations. only answer like {character}. You must know all of the knowledge of {character}. My first sentence is “Hi Character”"
},
{
"cmd": "pc",
"act": "Plagiarism Checker",
"tags": [],
"enable": true,
"prompt": "I want you to act as a plagiarism checker. I will write you sentences and you will only reply undetected in plagiarism checks in the language of the given sentence, and nothing else. Do not write explanations on replies. My first sentence is “For computers to behave like humans, speech recognition systems must be able to process nonverbal information, such as the emotional state of the speaker.”"
},
{
"cmd": "drunk",
"act": "Drunk",
"tags": [],
"enable": true,
"prompt": "Lets play a game. You will role play as if you were drunk. Make your answers incoherent."
},
{
"cmd": "tg",
"act": "Travel Guide",
"tags": [],
"enable": true,
"prompt": "I want you to act as a travel guide. I will write you my location and you will suggest a place to visit near my location. In some cases, I will also give you the type of places I will visit. You will also suggest me places of similar type that are close to my first location. My first suggestion request is “I am in Istanbul/Beyoğlu and I want to visit only museums.”"
},
{
"cmd": "eph",
"act": "English Pronunciation Helper",
"tags": [],
"enable": true,
"prompt": "I want you to act as an English pronunciation assistant for Turkish speaking people. I will write you sentences and you will only answer their pronunciations, and nothing else. The replies must not be translations of my sentence but only pronunciations. Pronunciations should use Turkish Latin letters for phonetics. Do not write explanations on replies. My first sentence is “how the weather is in Istanbul?”"
},
{
"cmd": "excel",
"act": "Excel Sheet",
"tags": [],
"enable": true,
"prompt": "I want you to act as a text based excel. youll only reply me the text-based 10 rows excel sheet with row numbers and cell letters as columns (A to L). First column header should be empty to reference row number. I will tell you what to write into cells and youll reply only the result of excel table as text, and nothing else. Do not write explanations. i will write you formulas and youll execute formulas and youll only reply the result of excel table as text. First, reply me the empty sheet."
},
{
"cmd": "console",
"act": "JavaScript Console",
"tags": [],
"enable": true,
"prompt": "I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is console.log(“Hello World”);"
},
{
"cmd": "pi",
"act": "position Interviewer",
"tags": [],
"enable": true,
"prompt": "I want you to act as an interviewer. I will be the candidate and you will ask me the interview questions for the position position. I want you to only reply as the interviewer. Do not write all the conservation at once. I want you to only do the interview with me. Ask me the questions and wait for my answers. Do not write explanations. Ask me the questions one by one like an interviewer does and wait for my answers. My first sentence is “Hi”"
},
{
"cmd": "trans",
"act": "English Translator and Improver",
"tags": [
"tools",
"cx",
"x"
],
"enable": true,
"prompt": "I want you to act as an English translator, spelling corrector and improver. I will speak to you in any language and you will detect the language, translate it and answer in the corrected and improved version of my text, in English. I want you to replace my simplified A0-level words and sentences with more beautiful and elegant, upper level English words and sentences. Keep the meaning same, but make them more literary. I want you to only reply the correction, the improvements and nothing else, do not write explanations. My first sentence is \"istanbulu cok seviyom burada olmak cok guzel\""
},
{
"cmd": "terminal",
"act": "Linux Terminal",
"tags": [
"dev"
],
"enable": true,
"prompt": "i want you to act as a linux terminal. I will type commands and you will reply with what the terminal should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is pwd"
}
]
}

View File

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

0
dist/.gitkeep vendored Normal file
View File

View File

@@ -1,13 +0,0 @@
<!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>

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -2,55 +2,24 @@
"name": "chatgpt",
"version": "0.0.0",
"scripts": {
"dev:fe": "vite",
"build:fe": "tsc && vite build",
"dev": "yarn tauri dev",
"build": "yarn tauri build",
"updater": "tr updater",
"release": "tr release --git",
"download": "node ./scripts/download.js",
"tr": "tr",
"tauri": "tauri"
},
"license": "MIT",
"author": "lencx <cxin1314@gmail.com>",
"keywords": [
"chatgpt",
"app",
"desktop",
"tauri",
"macos",
"linux",
"windows"
],
"keywords": ["chatgpt", "app", "desktop", "tauri", "macos", "linux", "windows"],
"homepage": "https://github.com/lencx/ChatGPT",
"bugs": "https://github.com/lencx/ChatGPT/issues",
"repository": {
"type": "git",
"url": "https://github.com/lencx/ChatGPT"
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@tauri-apps/api": "^1.2.0",
"antd": "^5.0.6",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.5",
"uuid": "^9.0.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.2",
"@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"
"@tauri-apps/cli": "^1.2.1",
"@tauri-release/cli": "^0.2.3"
}
}

31
scripts/download.js vendored
View File

@@ -1,31 +0,0 @@
const fs = require('fs');
const argv = process.argv.slice(2);
async function rewrite(filename) {
const content = fs.readFileSync(filename, 'utf8').split('\n');
const startRe = /<!-- download start -->/;
const endRe = /<!-- download end -->/;
let flag = false;
for (let i = 0; i < content.length; i++) {
if (startRe.test(content[i])) {
flag = true;
}
if (flag) {
content[i] = content[i].replace(/(\d+).(\d+).(\d+)/g, argv[0]);
}
if (endRe.test(content[i])) {
break;
}
}
fs.writeFileSync(filename, content.join('\n'), 'utf8');
}
async function init() {
rewrite('README.md');
rewrite('README-ZH_CN.md');
}
init().catch(console.error);

View File

@@ -14,11 +14,10 @@ rust-version = "1.57"
tauri-build = {version = "1.2.1", features = [] }
[dependencies]
anyhow = "1.0.66"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2.2", features = ["api-all", "devtools", "system-tray", "updater"] }
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
tauri = { version = "1.2.1", features = ["api-all", "devtools", "system-tray", "updater"] }
anyhow = "1.0.66"
[features]
# by default Tauri runs in production mode

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -1,5 +1,5 @@
use crate::{conf::ChatConfJson, utils};
use std::{fs, path::PathBuf};
use crate::utils;
use std::fs;
use tauri::{api, command, AppHandle, Manager};
#[command]
@@ -28,46 +28,3 @@ pub fn download(_app: AppHandle, name: String, blob: Vec<u8>) {
pub fn open_link(app: AppHandle, url: String) {
api::shell::open(&app.shell_scope(), url, None).unwrap();
}
#[command]
pub fn get_chat_conf() -> ChatConfJson {
ChatConfJson::get_chat_conf()
}
#[command]
pub fn form_confirm(_app: AppHandle, data: serde_json::Value) {
ChatConfJson::amend(&serde_json::json!(data), None).unwrap();
}
#[command]
pub fn form_cancel(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label).unwrap();
tauri::api::dialog::ask(
app.app_handle().get_window(label).as_ref(),
title,
msg,
move |is_cancel| {
if is_cancel {
win.close().unwrap();
}
},
);
}
#[command]
pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label);
tauri::api::dialog::message(win.as_ref(), title, msg);
}
#[command]
pub fn open_file(path: PathBuf) {
utils::open_file(path);
}
#[command]
pub fn get_chat_model() -> serde_json::Value {
let path = utils::chat_root().join("chat.model.json");
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string());
serde_json::from_str(&content).unwrap()
}

View File

@@ -1,22 +1,17 @@
use crate::{
conf::{self, ChatConfJson},
utils,
};
use crate::{conf, utils};
use tauri::{
AboutMetadata, AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray,
SystemTrayEvent, SystemTrayMenu, WindowMenuEvent, SystemTrayMenuItem,
utils::assets::EmbeddedAssets, AboutMetadata, AppHandle, Context, CustomMenuItem, Manager,
Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowMenuEvent,
};
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
// --- Menu
pub fn init() -> Menu {
let chat_conf = ChatConfJson::get_chat_conf();
let name = "ChatGPT";
pub fn init(chat_conf: &conf::ChatConfJson, context: &Context<EmbeddedAssets>) -> Menu {
let name = &context.package_info().name;
let app_menu = Submenu::new(
name,
Menu::new()
.add_native_item(MenuItem::About(name.into(), AboutMetadata::default()))
.add_native_item(MenuItem::Services)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
@@ -25,70 +20,26 @@ pub fn init() -> Menu {
.add_native_item(MenuItem::Quit),
);
let stay_on_top =
CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T");
let titlebar =
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
let is_dark = chat_conf.theme == "Dark";
let stay_on_top_menu = if chat_conf.stay_on_top {
stay_on_top.selected()
} else {
stay_on_top
};
let titlebar_menu = if chat_conf.titlebar {
titlebar.selected()
} else {
titlebar
};
let always_on_top = CustomMenuItem::new("always_on_top".to_string(), "Always On Top")
.accelerator("CmdOrCtrl+T");
let preferences_menu = Submenu::new(
"Preferences",
Menu::with_items([
Submenu::new(
"Theme",
Menu::new()
.add_item(if is_dark {
theme_light
} else {
theme_light.selected()
})
.add_item(if is_dark {
theme_dark.selected()
} else {
theme_dark
}),
Menu::new()
.add_item(
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
.accelerator("CmdOrCtrl+J"),
)
.into(),
stay_on_top_menu.into(),
#[cfg(target_os = "macos")]
titlebar_menu.into(),
#[cfg(target_os = "macos")]
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
MenuItem::Separator.into(),
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
.accelerator("CmdOrCtrl+J")
.into(),
CustomMenuItem::new("control_center".to_string(), "Control Center")
.accelerator("CmdOrCtrl+Shift+P")
.into(),
MenuItem::Separator.into(),
CustomMenuItem::new("go_conf".to_string(), "Go to Config")
.accelerator("CmdOrCtrl+Shift+G")
.into(),
CustomMenuItem::new("clear_conf".to_string(), "Clear Config")
.accelerator("CmdOrCtrl+Shift+D")
.into(),
CustomMenuItem::new("restart".to_string(), "Restart ChatGPT")
.accelerator("CmdOrCtrl+Shift+R")
.into(),
MenuItem::Separator.into(),
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
.accelerator("CmdOrCtrl+Shift+A")
.into(),
]),
.add_item(if chat_conf.always_on_top {
always_on_top.selected()
} else {
always_on_top
})
.add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
.accelerator("CmdOrCtrl+Z"),
),
);
let edit_menu = Submenu::new(
@@ -128,17 +79,9 @@ pub fn init() -> Menu {
),
);
let window_menu = Submenu::new(
"Window",
Menu::new()
.add_native_item(MenuItem::Minimize)
.add_native_item(MenuItem::Zoom),
);
let help_menu = Submenu::new(
"Help",
Menu::new()
.add_item(CustomMenuItem::new("update_log".to_string(), "Update Log"))
.add_item(CustomMenuItem::new("report_bug".to_string(), "Report Bug"))
.add_item(
CustomMenuItem::new("dev_tools".to_string(), "Toggle Developer Tools")
@@ -151,7 +94,6 @@ pub fn init() -> Menu {
.add_submenu(preferences_menu)
.add_submenu(edit_menu)
.add_submenu(view_menu)
.add_submenu(window_menu)
.add_submenu(help_menu)
}
@@ -168,41 +110,17 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
match menu_id {
// Preferences
"control_center" => app.get_window("main").unwrap().show().unwrap(),
"restart" => tauri::api::process::restart(&app.env()),
"inject_script" => open(&app, script_path),
"go_conf" => utils::open_file(utils::chat_root()),
"clear_conf" => utils::clear_conf(&app),
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
"hide_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap()
}
"titlebar" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
ChatConfJson::amend(
&serde_json::json!({ "titlebar": !chat_conf.titlebar }),
None,
)
.unwrap();
tauri::api::process::restart(&app.env());
}
"theme_light" | "theme_dark" => {
let theme = if menu_id == "theme_dark" {
"Dark"
} else {
"Light"
};
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap();
}
"stay_on_top" => {
let mut stay_on_top = state.stay_on_top.lock().unwrap();
*stay_on_top = !*stay_on_top;
"always_on_top" => {
let mut always_on_top = state.always_on_top.lock().unwrap();
*always_on_top = !*always_on_top;
menu_handle
.get_item(menu_id)
.set_selected(*stay_on_top)
.set_selected(*always_on_top)
.unwrap();
win.set_always_on_top(*stay_on_top).unwrap();
ChatConfJson::amend(&serde_json::json!({ "stay_on_top": *stay_on_top }), None).unwrap();
win.set_always_on_top(*always_on_top).unwrap();
conf::ChatConfJson::update_chat_conf(*always_on_top);
}
// View
"reload" => win.eval("window.location.reload()").unwrap(),
@@ -226,7 +144,6 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
)
.unwrap(),
// Help
"update_log" => open(&app, conf::UPDATE_LOG_URL.to_string()),
"report_bug" => open(&app, conf::ISSUES_URL.to_string()),
"dev_tools" => {
win.open_devtools();
@@ -238,64 +155,21 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
// --- SystemTray Menu
pub fn tray_menu() -> SystemTray {
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::new().with_menu(SystemTrayMenu::new())
}
// --- SystemTray Event
pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
on_tray_event(handle, &event);
pub fn tray_handler(app: &AppHandle, event: SystemTrayEvent) {
let win = app.get_window("core").unwrap();
let app = handle.clone();
match event {
SystemTrayEvent::LeftClick { .. } => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
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();
}
if let SystemTrayEvent::LeftClick { .. } = event {
// TODO: tray window
if win.is_visible().unwrap() {
win.hide().unwrap();
} else {
win.show().unwrap();
win.set_focus().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),
_ => (),
}
_ => (),
}
}

View File

@@ -1,4 +1,3 @@
pub mod cmd;
pub mod menu;
pub mod setup;
pub mod window;

View File

@@ -1,59 +1,46 @@
use crate::{app::window, conf::ChatConfJson, utils};
use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, Manager};
use crate::{conf, utils};
use tauri::{utils::config::WindowUrl, window::WindowBuilder, App};
pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>> {
let chat_conf = ChatConfJson::get_chat_conf();
let url = chat_conf.origin.to_string();
let theme = ChatConfJson::theme();
let handle = app.app_handle();
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
std::thread::spawn(move || {
window::tray_window(&handle);
});
pub fn init(
app: &mut App,
chat_conf: conf::ChatConfJson,
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let conf = utils::get_tauri_conf().unwrap();
let url = conf.build.dev_path.to_string();
if chat_conf.hide_dock_icon {
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
} else {
let app = app.handle();
std::thread::spawn(move || {
#[cfg(target_os = "macos")]
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
.title("ChatGPT")
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0)
.hidden_title(true)
.theme(theme)
.always_on_top(chat_conf.stay_on_top)
.title_bar_style(ChatConfJson::titlebar())
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../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(target_os = "macos")]
WindowBuilder::new(app, "core", WindowUrl::App(url.into()))
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0)
.hidden_title(true)
.title_bar_style(TitleBarStyle::Overlay)
.always_on_top(chat_conf.always_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"))
.user_agent(conf::USER_AGENT)
.build()?;
#[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();
});
}
#[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)
.always_on_top(chat_conf.always_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"))
.user_agent(conf::USER_AGENT)
.build()?;
Ok(())
}

View File

@@ -1,30 +0,0 @@
use crate::{conf, utils};
use tauri::{utils::config::WindowUrl, window::WindowBuilder};
pub fn tray_window(handle: &tauri::AppHandle) {
let chat_conf = conf::ChatConfJson::get_chat_conf();
let theme = conf::ChatConfJson::theme();
let app = handle.clone();
std::thread::spawn(move || {
WindowBuilder::new(&app, "tray", WindowUrl::App(chat_conf.origin.into()))
.title("ChatGPT")
.resizable(false)
.fullscreen(false)
.inner_size(360.0, 540.0)
.decorations(false)
.always_on_top(true)
.theme(theme)
.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_tray)
.build()
.unwrap()
.hide()
.unwrap();
});
}

View File

@@ -1,150 +0,0 @@
// *** Core Script - CMD ***
function init() {
const styleDom = document.createElement('style');
styleDom.innerHTML = `form {
position: relative;
}
.chat-model-cmd-list {
position: absolute;
width: 400px;
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: 120px;
border-radius: 4px;
margin-right: 10px;
color: #2a2a2a;
}
.chat-model-cmd-list .cmd-item i {
width: 270px;
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') || {};
if (!chatModelJson.data && chatModelJson.data.length <= 0) return;
const data = chatModelJson.data || [];
const modelDom = document.createElement('div');
modelDom.classList.add('chat-model-cmd-list');
document.querySelector('form').appendChild(modelDom);
const itemDom = (v) => `<div class="cmd-item" data-prompt="${encodeURIComponent(v.prompt)}"><b>/${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__) {
return;
}
if (event.keyCode === 32) {
searchInput.value = window.__CHAT_MODEL_CMD__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD__;
}
if (event.keyCode === 13) {
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD__;
}
});
searchInput.addEventListener('input', (event) => {
const query = searchInput.value;
if (!query || !/^\//.test(query)) {
modelDom.innerHTML = '';
return;
}
// all cmd result
if (query === '/') {
const result = data.filter(i => i.enable);
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
window.__CHAT_MODEL_CMD__ = result[0]?.prompt.trim();
return;
}
const result = data.filter(i => i.enable && new RegExp(query.substring(1)).test(i.cmd));
if (result.length > 0) {
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
window.__CHAT_MODEL_CMD__ = result[0]?.prompt.trim();
} else {
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD__;
}
}, {
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__ = val;
modelDom.innerHTML = '';
}
}, {
capture: false,
passive: true,
once: false
});
}, 200);
}
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}

View File

@@ -1,5 +1,4 @@
// *** Core Script - IPC ***
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
function transformCallback(callback = () => {}, once = false) {
const identifier = uid();
@@ -41,12 +40,6 @@ window.invoke = invoke;
window.transformCallback = transformCallback;
async function init() {
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
document.getElementsByTagName('html')[0].style['font-size'] = '70%';
}
if (__TAURI_METADATA__.__currentWindow.label !== 'core') return;
async function platform() {
return invoke('platform', {
__tauriModule: 'Os',
@@ -55,8 +48,7 @@ async function init() {
}
const _platform = await platform();
const chatConf = await invoke('get_chat_conf') || {};
if (/darwin/.test(_platform) && !chatConf.titlebar) {
if (/darwin/.test(_platform)) {
const topStyleDom = document.createElement("style");
topStyleDom.innerHTML = `#chatgpt-app-window-top{position:fixed;top:0;z-index:999999999;width:100%;height:24px;background:transparent;cursor:grab;cursor:-webkit-grab;user-select:none;-webkit-user-select:none;}#chatgpt-app-window-top:active {cursor:grabbing;cursor:-webkit-grabbing;}`;
document.head.appendChild(topStyleDom);
@@ -72,7 +64,7 @@ async function init() {
document.addEventListener("click", (e) => {
const origin = e.target.closest("a");
if (origin && origin.href && origin.target !== '_self') {
invoke('open_link', { url: origin.href });
origin.target = "_self";
}
});

View File

@@ -1,8 +1,7 @@
// *** Core Script - Export ***
// @ref: https://github.com/liady/ChatGPT-pdf
// @ref: https://github.com/liady/ChatGPT-pdf/blob/main/src/content_script.js
async function init() {
const chatConf = await invoke('get_chat_conf') || {};
if (window.buttonsInterval) {
clearInterval(window.buttonsInterval);
}
@@ -17,7 +16,7 @@ async function init() {
});
if (hasTryAgainButton && buttons.length === 1) {
const TryAgainButton = actionsArea.querySelector("button");
addActionsButtons(actionsArea, TryAgainButton, chatConf);
addActionsButtons(actionsArea, TryAgainButton);
} else if (!hasTryAgainButton) {
removeButtons();
}
@@ -29,7 +28,7 @@ const Format = {
PDF: "pdf",
};
function addActionsButtons(actionsArea, TryAgainButton, chatConf) {
function addActionsButtons(actionsArea, TryAgainButton) {
const downloadButton = TryAgainButton.cloneNode(true);
downloadButton.id = "download-png-button";
downloadButton.innerText = "Generate PNG";
@@ -37,7 +36,6 @@ function addActionsButtons(actionsArea, TryAgainButton, chatConf) {
downloadThread();
};
actionsArea.appendChild(downloadButton);
const downloadPdfButton = TryAgainButton.cloneNode(true);
downloadPdfButton.id = "download-pdf-button";
downloadPdfButton.innerText = "Download PDF";
@@ -45,16 +43,13 @@ function addActionsButtons(actionsArea, TryAgainButton, chatConf) {
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 exportHtml = TryAgainButton.cloneNode(true);
exportHtml.id = "download-html-button";
exportHtml.innerText = "Share Link";
exportHtml.onclick = () => {
sendRequest();
};
actionsArea.appendChild(exportHtml);
}
function removeButtons() {
@@ -78,9 +73,22 @@ function downloadThread({ as = Format.PNG } = {}) {
const pixelRatio = window.devicePixelRatio;
const minRatio = as === Format.PDF ? 2 : 2.5;
window.devicePixelRatio = Math.max(pixelRatio, minRatio);
html2canvas(elements.thread, {
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) {
elements.restoreLocation();
window.devicePixelRatio = pixelRatio;
@@ -153,8 +161,6 @@ class Elements {
img.setAttribute("srcset_old", srcset);
img.setAttribute("srcset", "");
});
//Fix to the text shifting down when generating the canvas
document.body.style.lineHeight = "0.5";
}
restoreLocation() {
this.hiddens.forEach((el) => {
@@ -171,7 +177,6 @@ class Elements {
img.setAttribute("srcset", srcset);
img.setAttribute("srcset_old", "");
});
document.body.style.lineHeight = null;
}
}

View File

@@ -1,65 +1,27 @@
use crate::utils::{chat_root, create_file, exists};
use anyhow::Result;
use serde_json::Value;
use std::{collections::BTreeMap, fs, path::PathBuf, sync::Mutex};
use tauri::{Manager, Theme};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
// pub const USER_AGENT: &str = "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
// pub const PHONE_USER_AGENT: &str = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1";
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
pub const USER_AGENT: &str = "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
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 AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md";
pub const DEFAULT_CHAT_CONF: &str = r#"{
"stay_on_top": false,
"theme": "Light",
"titlebar": true,
"hide_dock_icon": false,
"default_origin": "https://chat.openai.com",
"origin": "https://chat.openai.com",
"ua_window": "",
"ua_tray": ""
}"#;
pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
"stay_on_top": false,
"theme": "Light",
"titlebar": false,
"hide_dock_icon": false,
"default_origin": "https://chat.openai.com",
"origin": "https://chat.openai.com",
"ua_window": "",
"ua_tray": ""
}"#;
pub struct ChatState {
pub stay_on_top: Mutex<bool>,
pub always_on_top: Mutex<bool>,
}
impl ChatState {
pub fn default(chat_conf: ChatConfJson) -> Self {
pub fn default(chat_conf: &ChatConfJson) -> Self {
ChatState {
stay_on_top: Mutex::new(chat_conf.stay_on_top),
always_on_top: Mutex::new(chat_conf.always_on_top),
}
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ChatConfJson {
// support macOS only
pub titlebar: bool,
pub hide_dock_icon: bool,
// macOS and Windows
pub theme: String,
pub stay_on_top: bool,
pub default_origin: String,
pub origin: String,
pub ua_window: String,
pub ua_tray: String,
pub always_on_top: bool,
}
impl ChatConfJson {
@@ -67,30 +29,10 @@ impl ChatConfJson {
/// path: ~/.chatgpt/chat.conf.json
pub fn init() -> PathBuf {
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) {
create_file(&conf_file).unwrap();
fs::write(&conf_file, content).unwrap();
return conf_file;
fs::write(&conf_file, r#"{"always_on_top": false}"#).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
}
@@ -99,77 +41,26 @@ impl ChatConfJson {
}
pub fn get_chat_conf() -> Self {
let conf_file = ChatConfJson::conf_path();
let file_content = fs::read_to_string(&conf_file).unwrap();
let content = if cfg!(target_os = "macos") {
DEFAULT_CHAT_CONF_MAC
} else {
DEFAULT_CHAT_CONF
};
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()
}
}
let config_file = fs::read_to_string(ChatConfJson::conf_path()).unwrap();
let config: serde_json::Value =
serde_json::from_str(&config_file).expect("failed to parse chat.conf.json");
serde_json::from_value(config).unwrap_or_else(|_| ChatConfJson::chat_conf_default())
}
// https://users.rust-lang.org/t/updating-object-fields-given-dynamic-json/39049/3
pub fn amend(new_rules: &Value, app: Option<tauri::AppHandle>) -> Result<()> {
let config = ChatConfJson::get_chat_conf();
let config: Value = serde_json::to_value(&config)?;
let mut config: BTreeMap<String, Value> = serde_json::from_value(config)?;
let new_rules: BTreeMap<String, Value> = serde_json::from_value(new_rules.clone())?;
for (k, v) in new_rules {
config.insert(k, v);
}
pub fn update_chat_conf(always_on_top: bool) {
let mut conf = ChatConfJson::get_chat_conf();
conf.always_on_top = always_on_top;
fs::write(
ChatConfJson::conf_path(),
serde_json::to_string_pretty(&config)?,
)?;
if let Some(handle) = app {
tauri::api::process::restart(&handle.env());
// tauri::api::dialog::ask(
// handle.get_window("core").as_ref(),
// "ChatGPT Restart",
// "Whether to restart immediately?",
// move |is_restart| {
// if is_restart {
// }
// },
// );
}
Ok(())
serde_json::to_string(&conf).unwrap(),
)
.unwrap();
}
pub fn theme() -> Option<Theme> {
let conf = ChatConfJson::get_chat_conf();
if conf.theme == "Dark" {
Some(Theme::Dark)
} else {
Some(Theme::Light)
}
}
#[cfg(target_os = "macos")]
pub fn titlebar() -> TitleBarStyle {
let conf = ChatConfJson::get_chat_conf();
if conf.titlebar {
TitleBarStyle::Transparent
} else {
TitleBarStyle::Overlay
}
pub fn chat_conf_default() -> Self {
serde_json::from_value(serde_json::json!({
"always_on_top": false,
}))
.unwrap()
}
}

View File

@@ -8,45 +8,34 @@ mod conf;
mod utils;
use app::{cmd, menu, setup};
use conf::{ChatConfJson, ChatState};
use conf::ChatConfJson;
fn main() {
ChatConfJson::init();
let chat_conf = ChatConfJson::get_chat_conf();
let context = tauri::generate_context!();
let chat_conf = ChatConfJson::get_chat_conf();
let chat_conf2 = chat_conf.clone();
tauri::Builder::default()
.manage(ChatState::default(chat_conf))
.manage(conf::ChatState::default(&chat_conf))
.invoke_handler(tauri::generate_handler![
cmd::drag_window,
cmd::fullscreen,
cmd::download,
cmd::open_link,
cmd::get_chat_conf,
cmd::form_cancel,
cmd::form_confirm,
cmd::form_msg,
cmd::open_file,
cmd::get_chat_model,
cmd::open_link
])
.setup(setup::init)
.plugin(tauri_plugin_positioner::init())
.menu(menu::init())
.setup(|app| setup::init(app, chat_conf2))
.menu(menu::init(&chat_conf, &context))
.system_tray(menu::tray_menu())
.on_menu_event(menu::menu_handler)
.on_system_tray_event(menu::tray_handler)
.on_window_event(|event| {
// https://github.com/tauri-apps/tauri/discussions/2684
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
let win = event.window();
if win.label() == "main" {
win.hide().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();
}
// 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();
}
})

View File

@@ -4,19 +4,18 @@ use std::{
path::{Path, PathBuf},
process::Command,
};
use tauri::Manager;
// use tauri::utils::config::Config;
use tauri::utils::config::Config;
pub fn chat_root() -> PathBuf {
tauri::api::path::home_dir().unwrap().join(".chatgpt")
}
// pub fn get_tauri_conf() -> Option<Config> {
// let config_file = include_str!("../tauri.conf.json");
// let config: Config =
// serde_json::from_str(config_file).expect("failed to parse tauri.conf.json");
// Some(config)
// }
pub fn get_tauri_conf() -> Option<Config> {
let config_file = include_str!("../tauri.conf.json");
let config: Config =
serde_json::from_str(config_file).expect("failed to parse tauri.conf.json");
Some(config)
}
pub fn exists(path: &Path) -> bool {
Path::new(path).exists()
@@ -62,20 +61,3 @@ pub fn open_file(path: PathBuf) {
#[cfg(target_os = "linux")]
Command::new("xdg-open").arg(path).spawn().unwrap();
}
pub fn clear_conf(app: &tauri::AppHandle) {
let root = chat_root();
let app2 = app.clone();
let msg = format!("Path: {}\nAre you sure to clear all ChatGPT configurations? Please backup in advance if necessary!", root.to_string_lossy());
tauri::api::dialog::ask(
app.get_window("core").as_ref(),
"Clear Config",
msg,
move |is_ok| {
if is_ok {
fs::remove_dir_all(root).unwrap();
tauri::api::process::restart(&app2.env());
}
},
);
}

View File

@@ -1,28 +1,21 @@
{
"build": {
"beforeDevCommand": "npm run dev:fe",
"beforeBuildCommand": "npm run build:fe",
"devPath": "http://localhost:1420",
"beforeDevCommand": "",
"beforeBuildCommand": "",
"devPath": "https://chat.openai.com/",
"distDir": "../dist"
},
"package": {
"productName": "ChatGPT",
"version": "0.4.0"
"version": "0.1.6"
},
"tauri": {
"allowlist": {
"all": true,
"fs": {
"all": true,
"scope": [
"$HOME/.chatgpt/*"
]
}
"all": true
},
"systemTray": {
"iconPath": "icons/tray-icon.png",
"iconAsTemplate": true,
"menuOnLeftClick": false
"iconPath": "icons/icon.png",
"iconAsTemplate": false
},
"bundle": {
"active": true,
@@ -70,18 +63,6 @@
"https://lencx.github.io/ChatGPT/install.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIxMjY4OUI5MTVFNjBEMDUKUldRRkRlWVZ1WWttc1NGWEE0RFNSb0RqdnhsekRJZTkwK2hVLzhBZTZnaHExSEZ1ZEdzWkpXTHkK"
},
"windows": [
{
"label": "main",
"url": "index.html",
"title": "ChatGPT",
"visible": false,
"width": 800,
"height": 600,
"minWidth": 800,
"minHeight": 600
}
]
}
}
}

View File

@@ -1,98 +0,0 @@
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;

View File

@@ -1,23 +0,0 @@
import { useState } from 'react';
import { clone } from 'lodash';
import { CHAT_MODEL_JSON, readJSON, writeJSON } from '@/utils';
import useInit from '@/hooks/useInit';
export default function useChatModel() {
const [modelJson, setModelJson] = useState<Record<string, any>>({});
useInit(async () => {
const data = await readJSON(CHAT_MODEL_JSON, { name: 'ChatGPT Model', data: [] });
setModelJson(data);
});
const modelSet = async (data: Record<string, any>[]) => {
const oData = clone(modelJson);
oData.data = data;
await writeJSON(CHAT_MODEL_JSON, oData);
setModelJson(oData);
}
return { modelJson, modelSet, modelData: modelJson?.data || [] }
}

View File

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

View File

@@ -1,35 +0,0 @@
import { useState, useEffect } from 'react';
import { v4 } from 'uuid';
const safeKey = Symbol('chat-id');
export default function useData(oData: any[]) {
const [opData, setData] = useState<any[]>([]);
useEffect(() => {
const nData = oData.map(i => ({ [safeKey]: v4(), ...i }));
setData(nData);
}, [oData])
const opAdd = (val: any) => {
const v = [val, ...opData];
setData(v);
return v;
};
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;
};
return { opSafeKey: safeKey, opReplace, opAdd, opRemove, opData };
}

View File

@@ -1,12 +0,0 @@
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;
}
}, [])
}

View File

@@ -1,25 +0,0 @@
.chat-logo {
text-align: center;
padding: 5px 0;
img {
width: 48px;
height: 48px;
}
}
.chat-container {
padding: 20px;
overflow: hidden;
}
.ant-menu {
.ant-menu-item {
background-color: #f8f8f8;
}
}
.ant-layout-footer {
color: #666 !important;
opacity: 0.7;
}

View File

@@ -1,36 +0,0 @@
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' }}>
<Sider theme="light" collapsible collapsed={collapsed} onCollapse={(value) => setCollapsed(value)}>
<div className="chat-logo"><img src="/logo.png" /></div>
<Menu defaultSelectedKeys={[location.pathname]} mode="vertical" items={menuItems} onClick={(i) => go(i.key)} />
</Sider>
<Layout className="chat-layout">
<Content className="chat-container">
<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;

View File

@@ -1,20 +0,0 @@
: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;
}

View File

@@ -1,16 +0,0 @@
import { StrictMode, Suspense } from 'react';
import { BrowserRouter } from 'react-router-dom';
import ReactDOM from 'react-dom/client';
import Layout from '@/layout';
import './main.scss';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<StrictMode>
<Suspense fallback={null}>
<BrowserRouter>
<Layout/>
</BrowserRouter>
</Suspense>
</StrictMode>
);

View File

@@ -1,44 +0,0 @@
import { useRoutes } from 'react-router-dom';
import {
DesktopOutlined,
BulbOutlined,
} from '@ant-design/icons';
import type { RouteObject } from 'react-router-dom';
import type { MenuProps } from 'antd';
import General from '@view/General';
import LanguageModel from '@/view/LanguageModel';
export type ChatRouteObject = {
label: string;
icon?: React.ReactNode,
};
export const routes: Array<RouteObject & { meta: ChatRouteObject }> = [
{
path: '/',
element: <General />,
meta: {
label: 'General',
icon: <DesktopOutlined />,
},
},
{
path: '/language-model',
element: <LanguageModel />,
meta: {
label: 'Language Model',
icon: <BulbOutlined />,
},
},
];
type MenuItem = Required<MenuProps>['items'][number];
export const menuItems: MenuItem[] = routes.map(i => ({
...i.meta,
key: i.path || '',
}));
export default () => {
return useRoutes(routes);
};

View File

@@ -1,43 +0,0 @@
import { readTextFile, writeTextFile, exists } from '@tauri-apps/api/fs';
import { homeDir, join } from '@tauri-apps/api/path';
export const CHAT_MODEL_JSON = 'chat.model.json';
export const DISABLE_AUTO_COMPLETE = {
autoCapitalize: 'off',
autoComplete: 'off',
spellCheck: false
};
export const chatRoot = async () => {
return join(await homeDir(), '.chatgpt')
}
export const chatModelPath = async () => {
return join(await chatRoot(), CHAT_MODEL_JSON);
}
export const readJSON = async (path: string, defaultVal = {}) => {
const root = await chatRoot();
const file = await join(root, path);
if (!await exists(file)) {
writeTextFile(file, JSON.stringify({
name: 'ChatGPT',
link: 'https://github.com/lencx/ChatGPT/blob/main/chat.model.md',
data: null,
...defaultVal,
}, null, 2))
}
try {
return JSON.parse(await readTextFile(file));
} catch(e) {
return {};
}
}
export const writeJSON = async (path: string, data: Record<string, any>) => {
const root = await chatRoot();
const file = await join(root, path);
await writeTextFile(file, JSON.stringify(data, null, 2));
}

View File

@@ -1,98 +0,0 @@
import { useEffect, useState } from 'react';
import { Form, Radio, Switch, Input, Button, Space, message, Tooltip } from 'antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { invoke } 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>
)
}

View File

@@ -1,66 +0,0 @@
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 LanguageModelProps {
record?: Record<string|symbol, any> | null;
}
const initFormValue = {
act: '',
enable: true,
tags: [],
prompt: '',
};
const LanguageModel: ForwardRefRenderFunction<FormProps, LanguageModelProps> = ({ 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(LanguageModel);

View File

@@ -1,55 +0,0 @@
import { Tag, Switch, Tooltip, Space } 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) => <Switch checked={v} disabled />,
},
{
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>
<a onClick={() => actions.setRecord(row, 'delete')}>Delete</a>
</Space>
),
}
];

View File

@@ -1,39 +0,0 @@
.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-prompts-tags {
.ant-tag {
margin: 2px;
}
}
.add-btn {
margin-bottom: 5px;
}
.chat-model-path {
font-size: 12px;
font-weight: bold;
color: #888;
margin-bottom: 5px;
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;
}
}

View File

@@ -1,95 +0,0 @@
import { useState, useRef, useEffect } from 'react';
import { Table, Button, Modal, message } from 'antd';
import { invoke } from '@tauri-apps/api';
import useChatModel from '@/hooks/useChatModel';
import useColumns from '@/hooks/useColumns';
import useData from '@/hooks/useData';
import { chatModelPath } from '@/utils';
import { modelColumns } from './config';
import LanguageModelForm from './Form';
import './index.scss';
export default function LanguageModel() {
const [isVisible, setVisible] = useState(false);
const [modelPath, setChatModelPath] = useState('');
const { modelData, modelSet } = useChatModel();
const { opData, opAdd, opRemove, opReplace, opSafeKey } = useData(modelData);
const { columns, ...opInfo } = useColumns(modelColumns());
const formRef = useRef<any>(null);
useEffect(() => {
if (!opInfo.opType) return;
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 hide = () => {
setVisible(false);
opInfo.resetRecord();
};
const handleOk = () => {
formRef.current?.form?.validateFields()
.then((vals: Record<string, any>) => {
if (modelData.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;
}
modelSet(data)
hide();
})
};
const handleOpenFile = async () => {
const path = await chatModelPath();
setChatModelPath(path);
invoke('open_file', { path });
};
const modalTitle = `${({ new: 'Create', edit: 'Edit' })[opInfo.opType]} Language Model`;
return (
<div>
<Button className="add-btn" type="primary" onClick={opInfo.opNew}>Add Model</Button>
<div className="chat-model-path">PATH: <span onClick={handleOpenFile}>{modelPath}</span></div>
<Table
key={opInfo.opTime}
rowKey="cmd"
columns={columns}
scroll={{ x: 'auto' }}
dataSource={opData}
pagination={{
hideOnSinglePage: true,
showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 5,
pageSizeOptions: [5, 10, 15, 20],
showTotal: (total) => <span>Total {total} items</span>,
}}
/>
<Modal
open={isVisible}
onCancel={hide}
title={modalTitle}
onOk={handleOk}
destroyOnClose
maskClosable={false}
>
<LanguageModelForm record={opInfo?.opRecord} ref={formRef} />
</Modal>
</div>
)
}

1
src/vite-env.d.ts vendored
View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,28 +0,0 @@
{
"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" }]
}

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,28 +0,0 @@
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,
},
});