mirror of
https://github.com/FranP-code/ChatGPT.git
synced 2025-10-13 00:13:25 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2319f2fda | ||
|
|
9ec69631f3 | ||
|
|
be9846dc22 | ||
|
|
f071e0d6bc | ||
|
|
2f8ff36638 | ||
|
|
38e319a215 | ||
|
|
05057d06ad | ||
|
|
413d3354c7 | ||
|
|
f1c7fff800 | ||
|
|
6fe90dea5b | ||
|
|
25ab2b0368 | ||
|
|
94973b1420 | ||
|
|
0930cd782a | ||
|
|
0733bba4bf | ||
|
|
bf623365da | ||
|
|
dc88ea9182 | ||
|
|
f411541a76 | ||
|
|
ca3badc783 | ||
|
|
d7328f576a | ||
|
|
eaf72e2b73 | ||
|
|
bd2c4fff5c | ||
|
|
3ca66cf309 | ||
|
|
44c91bc85c | ||
|
|
a75ae5e615 | ||
|
|
8193104853 | ||
|
|
11e07e87d4 | ||
|
|
7b8f29534b | ||
|
|
e4e56c7dbb | ||
|
|
8a79c28398 | ||
|
|
a7c4545dbf | ||
|
|
d93079f682 | ||
|
|
44dcdba10f | ||
|
|
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 |
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "using version tag ${GITHUB_REF:10}"
|
echo "using version tag ${GITHUB_REF:10}"
|
||||||
echo ::set-output name=version::"${GITHUB_REF:10}"
|
echo "version=${GITHUB_REF:10}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
@@ -27,8 +27,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
tag_name: '${{ steps.get_version.outputs.VERSION }}'
|
tag_name: '${{ env.version }}'
|
||||||
release_name: 'ChatGPT ${{ steps.get_version.outputs.VERSION }}'
|
release_name: 'ChatGPT ${{ env.version }}'
|
||||||
body: 'See the assets to download this version and install.'
|
body: 'See the assets to download this version and install.'
|
||||||
|
|
||||||
build-tauri:
|
build-tauri:
|
||||||
@@ -61,19 +61,6 @@ jobs:
|
|||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: Get yarn cache directory path
|
|
||||||
id: yarn-cache-dir-path
|
|
||||||
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
|
|
||||||
|
|
||||||
- name: Yarn cache
|
|
||||||
uses: actions/cache@v2
|
|
||||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
|
||||||
with:
|
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-yarn-
|
|
||||||
|
|
||||||
- name: Install app dependencies and build it
|
- name: Install app dependencies and build it
|
||||||
run: yarn && yarn build:fe
|
run: yarn && yarn build:fe
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
|
|
||||||
**最新版:**
|
**最新版:**
|
||||||
|
|
||||||
- `Mac`: [ChatGPT_0.4.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.0_x64.dmg)
|
- `Mac`: [ChatGPT_0.6.9_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.6.9/ChatGPT_0.6.9_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)
|
- `Linux`: [chat-gpt_0.6.9_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.6.9/chat-gpt_0.6.9_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)
|
- `Windows`: [ChatGPT_0.6.9_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.6.9/ChatGPT_0.6.9_x64_en-US.msi)
|
||||||
|
|
||||||
[其他版本...](https://github.com/lencx/ChatGPT/releases)
|
[其他版本...](https://github.com/lencx/ChatGPT/releases)
|
||||||
|
|
||||||
@@ -34,18 +34,18 @@
|
|||||||
|
|
||||||
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 }
|
||||||
~~~
|
```
|
||||||
|
|
||||||
## 📢 公告
|
## 📢 公告
|
||||||
|
|
||||||
@@ -60,11 +60,10 @@ cask "popcorn-time", args: { "no-quarantine": true }
|
|||||||
|
|
||||||
数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。
|
数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。
|
||||||
|
|
||||||
项目会维护一份常用命令,您也可以直接将 [chat.model.json](https://github.com/lencx/ChatGPT/blob/main/chat.model.json) 复制到你的本地目录 `~/.chatgpt/chat.model.json`。
|
在 ChatGPT 文本输入区域,键入 `/` 开头的字符,则会弹出指令提示,按下空格键,它会默认将命令关联的文本填充到输入区域(注意:如果包含多个指令提示,它只会选择第一个作为填充,你可以持续输入,直到第一个提示命令为你想要时,再按下空格键。或者使用鼠标来点击多条指令中的某一个)。填充完成后,你只需要按下回车键即可。斜杠命令下,使用 TAB 键修改 `{q}` 标签内容(仅支持单个修改 [#54](https://github.com/lencx/ChatGPT/issues/54))。
|
||||||
|
|
||||||
在 ChatGPT 文本输入区域,键入 `/` 开头的字符,则会弹出指令提示,按下空格键,它会默认将命令关联的文本填充到输入区域(注意:如果包含多个指令提示,它只会选择第一个作为填充,你可以持续输入,直到第一个提示命令为你想要时,再按下空格键。或者使用鼠标来点击多条指令中的某一个)。填充完成后,你只需要按下回车键即可。
|
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## ✨ 功能概览
|
## ✨ 功能概览
|
||||||
|
|
||||||
@@ -74,6 +73,7 @@ cask "popcorn-time", args: { "no-quarantine": true }
|
|||||||
- 丰富的快捷键
|
- 丰富的快捷键
|
||||||
- 系统托盘悬浮窗
|
- 系统托盘悬浮窗
|
||||||
- 应用菜单功能强大
|
- 应用菜单功能强大
|
||||||
|
- 支持斜杠命令及其配置(可手动配置或从文件同步 [#55](https://github.com/lencx/ChatGPT/issues/55))
|
||||||
|
|
||||||
### 菜单项
|
### 菜单项
|
||||||
|
|
||||||
@@ -99,18 +99,68 @@ cask "popcorn-time", args: { "no-quarantine": true }
|
|||||||
- `Report Bug`: 报告 BUG 或反馈建议
|
- `Report Bug`: 报告 BUG 或反馈建议
|
||||||
- `Toggle Developer Tools`: 网站调试工具,调试页面或脚本可能需要
|
- `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/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/tray-login.png" alt="tray login"> <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">
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<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>
|
<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
|
||||||
|
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -11,6 +11,7 @@
|
|||||||
[](https://github.com/lencx/ChatGPT/releases)
|
[](https://github.com/lencx/ChatGPT/releases)
|
||||||
[](https://discord.gg/aPhCRf4zZr)
|
[](https://discord.gg/aPhCRf4zZr)
|
||||||
[](https://twitter.com/lencx_)
|
[](https://twitter.com/lencx_)
|
||||||
|
|
||||||
<!-- [](./README-ZH.md) -->
|
<!-- [](./README-ZH.md) -->
|
||||||
|
|
||||||
[Awesome ChatGPT](./AWESOME.md)
|
[Awesome ChatGPT](./AWESOME.md)
|
||||||
@@ -23,9 +24,9 @@
|
|||||||
|
|
||||||
**Latest:**
|
**Latest:**
|
||||||
|
|
||||||
- `Mac`: [ChatGPT_0.4.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.0_x64.dmg)
|
- `Mac`: [ChatGPT_0.6.9_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.6.9/ChatGPT_0.6.9_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)
|
- `Linux`: [chat-gpt_0.6.9_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.6.9/chat-gpt_0.6.9_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)
|
- `Windows`: [ChatGPT_0.6.9_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.6.9/ChatGPT_0.6.9_x64_en-US.msi)
|
||||||
|
|
||||||
[Other version...](https://github.com/lencx/ChatGPT/releases)
|
[Other version...](https://github.com/lencx/ChatGPT/releases)
|
||||||
|
|
||||||
@@ -35,18 +36,18 @@
|
|||||||
|
|
||||||
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 "chatgpt", args: { "no-quarantine": true }
|
||||||
~~~
|
```
|
||||||
|
|
||||||
## 📢 Announcement
|
## 📢 Announcement
|
||||||
|
|
||||||
@@ -61,11 +62,10 @@ You can look at [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-p
|
|||||||
|
|
||||||
After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`).
|
After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`).
|
||||||
|
|
||||||
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. 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)).
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ In the chatgpt text input area, type a character starting with `/` to bring up t
|
|||||||
- Common shortcut keys
|
- Common shortcut keys
|
||||||
- System tray hover window
|
- System tray hover window
|
||||||
- Powerful menu items
|
- Powerful menu items
|
||||||
|
- Support for slash commands and their configuration (can be configured manually or synchronized from a file [#55](https://github.com/lencx/ChatGPT/issues/55))
|
||||||
|
|
||||||
### MenuItem
|
### MenuItem
|
||||||
|
|
||||||
@@ -100,10 +101,61 @@ In the chatgpt text input area, type a character starting with `/` to bring up t
|
|||||||
- `Report Bug`: Report a bug or give feedback.
|
- `Report Bug`: Report a bug or give feedback.
|
||||||
- `Toggle Developer Tools`: Developer debugging tools.
|
- `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))
|
||||||
- Shortcut command typing chatgpt prompt
|
- `Control Center` - Feature Enhancements
|
||||||
- ...
|
- ...
|
||||||
|
|
||||||
## 👀 Preview
|
## 👀 Preview
|
||||||
|
|||||||
@@ -1,5 +1,42 @@
|
|||||||
# UPDATE LOG
|
# UPDATE LOG
|
||||||
|
|
||||||
|
## v0.6.9
|
||||||
|
|
||||||
|
fix: unable to synchronize
|
||||||
|
|
||||||
|
## v0.6.4
|
||||||
|
|
||||||
|
fix: path not allowed on the configured scope
|
||||||
|
|
||||||
|
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.6.0
|
||||||
|
|
||||||
|
fix:
|
||||||
|
- windows show Chinese when upgrading
|
||||||
|
|
||||||
|
## 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
|
## v0.4.0
|
||||||
|
|
||||||
feat:
|
feat:
|
||||||
|
|||||||
BIN
assets/chatgpt-cmd.gif
Normal file
BIN
assets/chatgpt-cmd.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 MiB |
@@ -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": "Let’s play a game. You will role play as if you were drunk. Make your answer’s 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. you’ll 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 you’ll reply only the result of excel table as text, and nothing else. Do not write explanations. i will write you formulas and you’ll execute formulas and you’ll 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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.8.0",
|
"@ant-design/icons": "^4.8.0",
|
||||||
"@tauri-apps/api": "^1.2.0",
|
"@tauri-apps/api": "^1.2.0",
|
||||||
"antd": "^5.0.6",
|
"antd": "^5.1.0",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
@@ -19,6 +19,18 @@ 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"
|
||||||
|
regex = "1.7.0"
|
||||||
|
# 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::{self, exists}};
|
||||||
use std::{fs, path::PathBuf};
|
use std::{collections::HashMap, fs, path::PathBuf};
|
||||||
use tauri::{api, command, AppHandle, Manager};
|
use tauri::{api, command, AppHandle, Manager};
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
@@ -66,8 +66,135 @@ pub fn open_file(path: PathBuf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub fn get_chat_model() -> serde_json::Value {
|
pub fn get_chat_model_cmd() -> serde_json::Value {
|
||||||
let path = utils::chat_root().join("chat.model.json");
|
let path = utils::chat_root().join("chat.model.cmd.json");
|
||||||
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string());
|
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string());
|
||||||
serde_json::from_str(&content).unwrap()
|
serde_json::from_str(&content).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct PromptRecord {
|
||||||
|
pub cmd: Option<String>,
|
||||||
|
pub act: String,
|
||||||
|
pub prompt: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub fn parse_prompt(data: String) -> Vec<PromptRecord> {
|
||||||
|
let mut rdr = csv::Reader::from_reader(data.as_bytes());
|
||||||
|
let mut list = vec![];
|
||||||
|
for result in rdr.deserialize() {
|
||||||
|
let record: PromptRecord = result.unwrap();
|
||||||
|
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 utils::chat_root;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub fn sync_prompts(app: AppHandle, data: String, time: u64) {
|
||||||
|
let data = parse_prompt(data)
|
||||||
|
.iter()
|
||||||
|
.map(move |i| ModelRecord {
|
||||||
|
cmd: if i.cmd.is_some() {
|
||||||
|
i.cmd.clone().unwrap()
|
||||||
|
} else {
|
||||||
|
utils::gen_cmd(i.act.clone())
|
||||||
|
},
|
||||||
|
act: i.act.clone(),
|
||||||
|
prompt: i.prompt.clone(),
|
||||||
|
tags: vec!["chatgpt-prompts".to_string()],
|
||||||
|
enable: true,
|
||||||
|
})
|
||||||
|
.collect::<Vec<ModelRecord>>();
|
||||||
|
|
||||||
|
let model = chat_root().join("chat.model.json");
|
||||||
|
let model_cmd = chat_root().join("chat.model.cmd.json");
|
||||||
|
let chatgpt_prompts = chat_root().join("cache_model").join("chatgpt_prompts.json");
|
||||||
|
|
||||||
|
if !exists(&model) {
|
||||||
|
fs::write(&model, serde_json::json!({
|
||||||
|
"name": "ChatGPT Model",
|
||||||
|
"link": "https://github.com/lencx/ChatGPT"
|
||||||
|
}).to_string()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// chatgpt_prompts.json
|
||||||
|
fs::write(
|
||||||
|
chatgpt_prompts,
|
||||||
|
serde_json::to_string_pretty(&data).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let cmd_data = cmd_list();
|
||||||
|
|
||||||
|
// chat.model.cmd.json
|
||||||
|
fs::write(
|
||||||
|
model_cmd,
|
||||||
|
serde_json::to_string_pretty(&serde_json::json!({
|
||||||
|
"name": "ChatGPT CMD",
|
||||||
|
"last_updated": time,
|
||||||
|
"data": cmd_data,
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut kv = HashMap::new();
|
||||||
|
kv.insert(
|
||||||
|
"sync_prompts".to_string(),
|
||||||
|
serde_json::json!({ "id": "chatgpt_prompts", "last_updated": time }),
|
||||||
|
);
|
||||||
|
let model_data = utils::merge(
|
||||||
|
&serde_json::from_str(&fs::read_to_string(&model).unwrap()).unwrap(),
|
||||||
|
&kv,
|
||||||
|
);
|
||||||
|
|
||||||
|
// chat.model.json
|
||||||
|
fs::write(model, serde_json::to_string_pretty(&model_data).unwrap()).unwrap();
|
||||||
|
|
||||||
|
// refresh window
|
||||||
|
api::dialog::message(
|
||||||
|
app.get_window("core").as_ref(),
|
||||||
|
"Sync Prompts",
|
||||||
|
"ChatGPT Prompts data has been synchronized!",
|
||||||
|
);
|
||||||
|
window_reload(app, "core");
|
||||||
|
}
|
||||||
|
|||||||
123
src-tauri/src/app/fs_extra.rs
Normal file
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()
|
||||||
|
// }
|
||||||
@@ -4,10 +4,12 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
AboutMetadata, AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray,
|
AboutMetadata, AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray,
|
||||||
SystemTrayEvent, SystemTrayMenu, WindowMenuEvent, SystemTrayMenuItem,
|
SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, WindowMenuEvent,
|
||||||
};
|
};
|
||||||
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
|
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
|
||||||
|
|
||||||
|
use super::window;
|
||||||
|
|
||||||
// --- Menu
|
// --- Menu
|
||||||
pub fn init() -> Menu {
|
pub fn init() -> Menu {
|
||||||
let chat_conf = ChatConfJson::get_chat_conf();
|
let chat_conf = ChatConfJson::get_chat_conf();
|
||||||
@@ -47,6 +49,10 @@ pub fn init() -> 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()
|
||||||
@@ -67,13 +73,11 @@ pub fn init() -> Menu {
|
|||||||
titlebar_menu.into(),
|
titlebar_menu.into(),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
|
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
|
||||||
MenuItem::Separator.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(),
|
||||||
CustomMenuItem::new("control_center".to_string(), "Control Center")
|
MenuItem::Separator.into(),
|
||||||
.accelerator("CmdOrCtrl+Shift+P")
|
CustomMenuItem::new("sync_prompts".to_string(), "Sync Prompts").into(),
|
||||||
.into(),
|
|
||||||
MenuItem::Separator.into(),
|
MenuItem::Separator.into(),
|
||||||
CustomMenuItem::new("go_conf".to_string(), "Go to Config")
|
CustomMenuItem::new("go_conf".to_string(), "Go to Config")
|
||||||
.accelerator("CmdOrCtrl+Shift+G")
|
.accelerator("CmdOrCtrl+Shift+G")
|
||||||
@@ -138,6 +142,10 @@ pub fn init() -> Menu {
|
|||||||
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(
|
||||||
@@ -168,12 +176,27 @@ 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(),
|
"control_center" => window::control_window(&app),
|
||||||
"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()),
|
"go_conf" => utils::open_file(utils::chat_root()),
|
||||||
"clear_conf" => utils::clear_conf(&app),
|
"clear_conf" => utils::clear_conf(&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("core").as_ref(),
|
||||||
|
"Sync Prompts",
|
||||||
|
"Data sync will enable all prompts, are you sure you want to sync?",
|
||||||
|
move |is_restart| {
|
||||||
|
if is_restart {
|
||||||
|
app.get_window("core")
|
||||||
|
.unwrap()
|
||||||
|
.eval("window.__sync_prompts && window.__sync_prompts()")
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
"hide_dock_icon" => {
|
"hide_dock_icon" => {
|
||||||
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap()
|
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap()
|
||||||
}
|
}
|
||||||
@@ -226,6 +249,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" => {
|
||||||
@@ -240,11 +264,20 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
|
|||||||
pub fn tray_menu() -> SystemTray {
|
pub fn tray_menu() -> SystemTray {
|
||||||
SystemTray::new().with_menu(
|
SystemTray::new().with_menu(
|
||||||
SystemTrayMenu::new()
|
SystemTrayMenu::new()
|
||||||
.add_item(CustomMenuItem::new("control_center".to_string(), "Control Center"))
|
.add_item(CustomMenuItem::new(
|
||||||
.add_item(CustomMenuItem::new("show_dock_icon".to_string(), "Show Dock Icon"))
|
"control_center".to_string(),
|
||||||
.add_item(CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon"))
|
"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_native_item(SystemTrayMenuItem::Separator)
|
||||||
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT"))
|
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,28 +306,22 @@ pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||||
"control_center" => app.get_window("main").unwrap().show().unwrap(),
|
"control_center" => window::control_window(&app),
|
||||||
"restart" => tauri::api::process::restart(&handle.env()),
|
"restart" => tauri::api::process::restart(&handle.env()),
|
||||||
"show_dock_icon" => {
|
"show_dock_icon" => {
|
||||||
ChatConfJson::amend(
|
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": false }), Some(app))
|
||||||
&serde_json::json!({ "hide_dock_icon": false }),
|
.unwrap();
|
||||||
Some(app),
|
}
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
},
|
|
||||||
"hide_dock_icon" => {
|
"hide_dock_icon" => {
|
||||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||||
if !chat_conf.hide_dock_icon {
|
if !chat_conf.hide_dock_icon {
|
||||||
ChatConfJson::amend(
|
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app))
|
||||||
&serde_json::json!({ "hide_dock_icon": true }),
|
.unwrap();
|
||||||
Some(app),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"quit" => std::process::exit(0),
|
"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;
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
|
|||||||
.initialization_script(include_str!("../assets/export.js"))
|
.initialization_script(include_str!("../assets/export.js"))
|
||||||
.initialization_script(include_str!("../assets/cmd.js"))
|
.initialization_script(include_str!("../assets/cmd.js"))
|
||||||
.user_agent(&chat_conf.ua_window)
|
.user_agent(&chat_conf.ua_window)
|
||||||
.build().unwrap();
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
|
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
|
||||||
@@ -51,7 +52,8 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
|
|||||||
.initialization_script(include_str!("../assets/export.js"))
|
.initialization_script(include_str!("../assets/export.js"))
|
||||||
.initialization_script(include_str!("../assets/cmd.js"))
|
.initialization_script(include_str!("../assets/cmd.js"))
|
||||||
.user_agent(&chat_conf.ua_window)
|
.user_agent(&chat_conf.ua_window)
|
||||||
.build().unwrap();
|
.build()
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,3 +28,17 @@ pub fn tray_window(handle: &tauri::AppHandle) {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn control_window(handle: &tauri::AppHandle) {
|
||||||
|
let app = handle.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
WindowBuilder::new(&app, "main", WindowUrl::App("index.html".into()))
|
||||||
|
.title("ChatGPT")
|
||||||
|
.resizable(true)
|
||||||
|
.fullscreen(false)
|
||||||
|
.inner_size(800.0, 600.0)
|
||||||
|
.min_inner_size(800.0, 600.0)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
98
src-tauri/src/assets/cmd.js
vendored
98
src-tauri/src/assets/cmd.js
vendored
@@ -7,7 +7,6 @@ function init() {
|
|||||||
}
|
}
|
||||||
.chat-model-cmd-list {
|
.chat-model-cmd-list {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 400px;
|
|
||||||
bottom: 60px;
|
bottom: 60px;
|
||||||
max-height: 100px;
|
max-height: 100px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -31,13 +30,17 @@ function init() {
|
|||||||
}
|
}
|
||||||
.chat-model-cmd-list .cmd-item b {
|
.chat-model-cmd-list .cmd-item b {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 120px;
|
width: 100px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
color: #2a2a2a;
|
color: #2a2a2a;
|
||||||
}
|
}
|
||||||
.chat-model-cmd-list .cmd-item i {
|
.chat-model-cmd-list .cmd-item i {
|
||||||
width: 270px;
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -58,35 +61,89 @@ function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cmdTip() {
|
async function cmdTip() {
|
||||||
const chatModelJson = await invoke('get_chat_model') || {};
|
const chatModelJson = await invoke('get_chat_model_cmd') || {};
|
||||||
if (!chatModelJson.data && chatModelJson.data.length <= 0) return;
|
const data = chatModelJson.data;
|
||||||
const data = chatModelJson.data || [];
|
if (data.length <= 0) return;
|
||||||
|
|
||||||
const modelDom = document.createElement('div');
|
const modelDom = document.createElement('div');
|
||||||
modelDom.classList.add('chat-model-cmd-list');
|
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);
|
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 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');
|
const searchInput = document.querySelector('form textarea');
|
||||||
|
|
||||||
// Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`.
|
// 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.
|
// If more than one command appears in the search results, the first one will be used by default.
|
||||||
searchInput.addEventListener('keydown', (event) => {
|
searchInput.addEventListener('keydown', (event) => {
|
||||||
if (!window.__CHAT_MODEL_CMD__) {
|
if (!window.__CHAT_MODEL_CMD_PROMPT__) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.keyCode === 32) {
|
// feat: https://github.com/lencx/ChatGPT/issues/54
|
||||||
searchInput.value = window.__CHAT_MODEL_CMD__;
|
if (event.keyCode === 9 && !window.__CHAT_MODEL_STATUS__) {
|
||||||
modelDom.innerHTML = '';
|
const strGroup = window.__CHAT_MODEL_CMD_PROMPT__.match(/\{([^{}]*)\}/) || [];
|
||||||
delete window.__CHAT_MODEL_CMD__;
|
|
||||||
|
if (strGroup[1]) {
|
||||||
|
searchInput.value = `/${window.__CHAT_MODEL_CMD__}` + ` {${strGroup[1]}}` + ' |-> ';
|
||||||
|
window.__CHAT_MODEL_STATUS__ = 1;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
if (event.keyCode === 13) {
|
|
||||||
|
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) {
|
||||||
|
searchInput.value = window.__CHAT_MODEL_CMD_PROMPT__;
|
||||||
modelDom.innerHTML = '';
|
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_CMD__;
|
||||||
|
delete window.__CHAT_MODEL_STATUS__;
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchInput.addEventListener('input', (event) => {
|
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;
|
const query = searchInput.value;
|
||||||
if (!query || !/^\//.test(query)) {
|
if (!query || !/^\//.test(query)) {
|
||||||
modelDom.innerHTML = '';
|
modelDom.innerHTML = '';
|
||||||
@@ -95,19 +152,22 @@ async function cmdTip() {
|
|||||||
|
|
||||||
// all cmd result
|
// all cmd result
|
||||||
if (query === '/') {
|
if (query === '/') {
|
||||||
const result = data.filter(i => i.enable);
|
modelDom.innerHTML = `<div>${data.map(itemDom).join('')}</div>`;
|
||||||
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
|
window.__CHAT_MODEL_CMD_PROMPT__ = data[0]?.prompt.trim();
|
||||||
window.__CHAT_MODEL_CMD__ = result[0]?.prompt.trim();
|
window.__CHAT_MODEL_CMD__ = data[0]?.cmd.trim();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = data.filter(i => i.enable && new RegExp(query.substring(1)).test(i.cmd));
|
const result = data.filter(i => new RegExp(query.substring(1)).test(i.cmd));
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
|
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
|
||||||
window.__CHAT_MODEL_CMD__ = result[0]?.prompt.trim();
|
window.__CHAT_MODEL_CMD_PROMPT__ = result[0]?.prompt.trim();
|
||||||
|
window.__CHAT_MODEL_CMD__ = result[0]?.cmd.trim();
|
||||||
} else {
|
} else {
|
||||||
modelDom.innerHTML = '';
|
modelDom.innerHTML = '';
|
||||||
|
delete window.__CHAT_MODEL_CMD_PROMPT__;
|
||||||
delete window.__CHAT_MODEL_CMD__;
|
delete window.__CHAT_MODEL_CMD__;
|
||||||
|
delete window.__CHAT_MODEL_STATUS__;
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
capture: false,
|
capture: false,
|
||||||
@@ -129,7 +189,7 @@ async function cmdTip() {
|
|||||||
const val = decodeURIComponent(item.getAttribute('data-prompt'));
|
const val = decodeURIComponent(item.getAttribute('data-prompt'));
|
||||||
searchInput.value = val;
|
searchInput.value = val;
|
||||||
document.querySelector('form textarea').focus();
|
document.querySelector('form textarea').focus();
|
||||||
window.__CHAT_MODEL_CMD__ = val;
|
window.__CHAT_MODEL_CMD_PROMPT__ = val;
|
||||||
modelDom.innerHTML = '';
|
modelDom.innerHTML = '';
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
20
src-tauri/src/assets/core.js
vendored
20
src-tauri/src/assets/core.js
vendored
@@ -86,6 +86,26 @@ async function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.__sync_prompts = async function() {
|
||||||
|
const res = await fetch('https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv');
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.text();
|
||||||
|
console.log('«94» /src/assets/core.js ~> ', data);
|
||||||
|
|
||||||
|
await invoke('sync_prompts', { data, time: Date.now() });
|
||||||
|
} else {
|
||||||
|
invoke('messageDialog', {
|
||||||
|
__tauriModule: 'Dialog',
|
||||||
|
message: {
|
||||||
|
cmd: 'messageDialog',
|
||||||
|
message: 'ChatGPT Prompts data sync failed, please try again!'.toString(),
|
||||||
|
title: 'Sync Prompts'.toString(),
|
||||||
|
type: 'error'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
105
src-tauri/src/assets/export.js
vendored
105
src-tauri/src/assets/export.js
vendored
@@ -1,6 +1,7 @@
|
|||||||
// *** Core Script - Export ***
|
// *** Core Script - Export ***
|
||||||
// @ref: https://github.com/liady/ChatGPT-pdf
|
// @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,6 +84,33 @@ 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();
|
||||||
@@ -113,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)) });
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -27,7 +59,12 @@ fn main() {
|
|||||||
cmd::form_confirm,
|
cmd::form_confirm,
|
||||||
cmd::form_msg,
|
cmd::form_msg,
|
||||||
cmd::open_file,
|
cmd::open_file,
|
||||||
cmd::get_chat_model,
|
cmd::get_chat_model_cmd,
|
||||||
|
cmd::parse_prompt,
|
||||||
|
cmd::sync_prompts,
|
||||||
|
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())
|
||||||
@@ -40,7 +77,7 @@ fn main() {
|
|||||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
|
||||||
let win = event.window();
|
let win = event.window();
|
||||||
if win.label() == "main" {
|
if win.label() == "main" {
|
||||||
win.hide().unwrap();
|
win.close().unwrap();
|
||||||
} else {
|
} else {
|
||||||
// TODO: https://github.com/tauri-apps/tauri/issues/3084
|
// TODO: https://github.com/tauri-apps/tauri/issues/3084
|
||||||
// event.window().hide().unwrap();
|
// event.window().hide().unwrap();
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use log::info;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
@@ -29,6 +33,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 +60,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();
|
||||||
|
|
||||||
@@ -79,3 +92,21 @@ pub fn clear_conf(app: &tauri::AppHandle) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn merge(v: &Value, fields: &HashMap<String, Value>) -> Value {
|
||||||
|
match v {
|
||||||
|
Value::Object(m) => {
|
||||||
|
let mut m = m.clone();
|
||||||
|
for (k, v) in fields {
|
||||||
|
m.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
Value::Object(m)
|
||||||
|
}
|
||||||
|
v => v.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_cmd(name: String) -> String {
|
||||||
|
let re = Regex::new(r"[^a-zA-Z0-9]").unwrap();
|
||||||
|
re.replace_all(&name, "_").to_lowercase()
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,15 +7,23 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "ChatGPT",
|
"productName": "ChatGPT",
|
||||||
"version": "0.4.0"
|
"version": "0.6.9"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"all": true,
|
"all": true,
|
||||||
|
"http": {
|
||||||
|
"all": true,
|
||||||
|
"scope": [
|
||||||
|
"https://**",
|
||||||
|
"http://**"
|
||||||
|
]
|
||||||
|
},
|
||||||
"fs": {
|
"fs": {
|
||||||
"all": true,
|
"all": true,
|
||||||
"scope": [
|
"scope": [
|
||||||
"$HOME/.chatgpt/*"
|
"$HOME/.chatgpt/**",
|
||||||
|
"$HOME/.chatgpt/cache_model/**"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -51,13 +59,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": {
|
||||||
@@ -70,18 +78,6 @@
|
|||||||
"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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
46
src/hooks/useChatModel.ts
vendored
46
src/hooks/useChatModel.ts
vendored
@@ -1,23 +1,53 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { clone } from 'lodash';
|
import { clone } from 'lodash';
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
|
||||||
import { CHAT_MODEL_JSON, readJSON, writeJSON } from '@/utils';
|
import { CHAT_MODEL_JSON, CHAT_MODEL_CMD_JSON, readJSON, writeJSON } from '@/utils';
|
||||||
import useInit from '@/hooks/useInit';
|
import useInit from '@/hooks/useInit';
|
||||||
|
|
||||||
export default function useChatModel() {
|
export default function useChatModel(key: string, file = CHAT_MODEL_JSON) {
|
||||||
const [modelJson, setModelJson] = useState<Record<string, any>>({});
|
const [modelJson, setModelJson] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
useInit(async () => {
|
useInit(async () => {
|
||||||
const data = await readJSON(CHAT_MODEL_JSON, { name: 'ChatGPT Model', data: [] });
|
const data = await readJSON(file, {
|
||||||
|
defaultVal: { name: 'ChatGPT Model', [key]: null },
|
||||||
|
});
|
||||||
setModelJson(data);
|
setModelJson(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
const modelSet = async (data: Record<string, any>[]) => {
|
const modelSet = async (data: Record<string, any>[]|Record<string, any>) => {
|
||||||
const oData = clone(modelJson);
|
const oData = clone(modelJson);
|
||||||
oData.data = data;
|
oData[key] = data;
|
||||||
await writeJSON(CHAT_MODEL_JSON, oData);
|
await writeJSON(file, oData);
|
||||||
setModelJson(oData);
|
setModelJson(oData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { modelJson, modelSet, modelData: modelJson?.data || [] }
|
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 };
|
||||||
}
|
}
|
||||||
30
src/hooks/useData.ts
vendored
30
src/hooks/useData.ts
vendored
@@ -1,15 +1,14 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
const safeKey = Symbol('chat-id');
|
export const safeKey = Symbol('chat-id');
|
||||||
|
|
||||||
export default function useData(oData: any[]) {
|
export default function useData(oData: any[]) {
|
||||||
const [opData, setData] = useState<any[]>([]);
|
const [opData, setData] = useState<any[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nData = oData.map(i => ({ [safeKey]: v4(), ...i }));
|
opInit(oData);
|
||||||
setData(nData);
|
}, [])
|
||||||
}, [oData])
|
|
||||||
|
|
||||||
const opAdd = (val: any) => {
|
const opAdd = (val: any) => {
|
||||||
const v = [val, ...opData];
|
const v = [val, ...opData];
|
||||||
@@ -17,6 +16,12 @@ export default function useData(oData: any[]) {
|
|||||||
return v;
|
return v;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const opInit = (val: any[] = []) => {
|
||||||
|
if (!val || !Array.isArray(val)) return;
|
||||||
|
const nData = val.map(i => ({ [safeKey]: v4(), ...i }));
|
||||||
|
setData(nData);
|
||||||
|
};
|
||||||
|
|
||||||
const opRemove = (id: string) => {
|
const opRemove = (id: string) => {
|
||||||
const nData = opData.filter(i => i[safeKey] !== id);
|
const nData = opData.filter(i => i[safeKey] !== id);
|
||||||
setData(nData);
|
setData(nData);
|
||||||
@@ -31,5 +36,20 @@ export default function useData(oData: any[]) {
|
|||||||
return nData;
|
return nData;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { opSafeKey: safeKey, opReplace, opAdd, opRemove, opData };
|
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 };
|
||||||
}
|
}
|
||||||
2
src/hooks/useInit.ts
vendored
2
src/hooks/useInit.ts
vendored
@@ -8,5 +8,5 @@ export default function useInit(callback: () => void) {
|
|||||||
callback();
|
callback();
|
||||||
isInit.current = false;
|
isInit.current = false;
|
||||||
}
|
}
|
||||||
}, [])
|
})
|
||||||
}
|
}
|
||||||
37
src/hooks/useTable.tsx
vendored
Normal file
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>,
|
||||||
|
};
|
||||||
7
src/layout/index.scss
vendored
7
src/layout/index.scss
vendored
@@ -8,12 +8,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-layout-sider-trigger {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-menu {
|
.ant-menu {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
.ant-menu-item {
|
.ant-menu-item {
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/layout/index.tsx
vendored
36
src/layout/index.tsx
vendored
@@ -17,13 +17,39 @@ const ChatLayout: FC<ChatLayoutProps> = ({ children }) => {
|
|||||||
const go = useNavigate();
|
const go = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<Layout style={{ minHeight: '100vh' }} hasSider>
|
||||||
<Sider theme="light" collapsible collapsed={collapsed} onCollapse={(value) => setCollapsed(value)}>
|
<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>
|
<div className="chat-logo"><img src="/logo.png" /></div>
|
||||||
<Menu defaultSelectedKeys={[location.pathname]} mode="vertical" items={menuItems} onClick={(i) => go(i.key)} />
|
<Menu
|
||||||
|
defaultSelectedKeys={[location.pathname]}
|
||||||
|
mode="inline"
|
||||||
|
inlineIndent={12}
|
||||||
|
items={menuItems}
|
||||||
|
defaultOpenKeys={['/model']}
|
||||||
|
onClick={(i) => go(i.key)}
|
||||||
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout className="chat-layout">
|
<Layout className="chat-layout" style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 300ms ease-out' }}>
|
||||||
<Content className="chat-container">
|
<Content
|
||||||
|
className="chat-container"
|
||||||
|
style={{
|
||||||
|
overflow: 'inherit'
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Routes />
|
<Routes />
|
||||||
</Content>
|
</Content>
|
||||||
<Footer style={{ textAlign: 'center' }}>
|
<Footer style={{ textAlign: 'center' }}>
|
||||||
|
|||||||
60
src/main.scss
vendored
60
src/main.scss
vendored
@@ -17,4 +17,64 @@
|
|||||||
html, body {
|
html, body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 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-table-tip {
|
||||||
|
> span {
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-sync-path {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #888;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
max-width: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
68
src/routes.tsx
vendored
68
src/routes.tsx
vendored
@@ -2,19 +2,32 @@ import { useRoutes } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
DesktopOutlined,
|
DesktopOutlined,
|
||||||
BulbOutlined,
|
BulbOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
FileSyncOutlined,
|
||||||
|
UserOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import type { RouteObject } from 'react-router-dom';
|
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
|
|
||||||
import General from '@view/General';
|
import General from '@view/General';
|
||||||
import LanguageModel from '@/view/LanguageModel';
|
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 ChatRouteObject = {
|
export type ChatRouteMetaObject = {
|
||||||
label: string;
|
label: string;
|
||||||
icon?: React.ReactNode,
|
icon?: React.ReactNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routes: Array<RouteObject & { meta: ChatRouteObject }> = [
|
type ChatRouteObject = {
|
||||||
|
path: string;
|
||||||
|
element?: JSX.Element;
|
||||||
|
hideMenu?: boolean;
|
||||||
|
meta?: ChatRouteMetaObject;
|
||||||
|
children?: ChatRouteObject[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routes: Array<ChatRouteObject> = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: <General />,
|
element: <General />,
|
||||||
@@ -24,20 +37,55 @@ export const routes: Array<RouteObject & { meta: ChatRouteObject }> = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/language-model',
|
path: '/model',
|
||||||
element: <LanguageModel />,
|
|
||||||
meta: {
|
meta: {
|
||||||
label: 'Language Model',
|
label: 'Language Model',
|
||||||
icon: <BulbOutlined />,
|
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];
|
type MenuItem = Required<MenuProps>['items'][number];
|
||||||
export const menuItems: MenuItem[] = routes.map(i => ({
|
export const menuItems: MenuItem[] = routes
|
||||||
...i.meta,
|
.filter((j) => !j.hideMenu)
|
||||||
key: i.path || '',
|
.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 () => {
|
export default () => {
|
||||||
return useRoutes(routes);
|
return useRoutes(routes);
|
||||||
|
|||||||
45
src/utils.ts
vendored
45
src/utils.ts
vendored
@@ -1,7 +1,11 @@
|
|||||||
import { readTextFile, writeTextFile, exists } from '@tauri-apps/api/fs';
|
import { readTextFile, writeTextFile, exists, createDir } from '@tauri-apps/api/fs';
|
||||||
import { homeDir, join } from '@tauri-apps/api/path';
|
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_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 = {
|
export const DISABLE_AUTO_COMPLETE = {
|
||||||
autoCapitalize: 'off',
|
autoCapitalize: 'off',
|
||||||
autoComplete: 'off',
|
autoComplete: 'off',
|
||||||
@@ -12,19 +16,27 @@ export const chatRoot = async () => {
|
|||||||
return join(await homeDir(), '.chatgpt')
|
return join(await homeDir(), '.chatgpt')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chatModelPath = async () => {
|
export const chatModelPath = async (): Promise<string> => {
|
||||||
return join(await chatRoot(), CHAT_MODEL_JSON);
|
return join(await chatRoot(), CHAT_MODEL_JSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const readJSON = async (path: string, defaultVal = {}) => {
|
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 root = await chatRoot();
|
||||||
const file = await join(root, path);
|
const file = await join(isRoot ? '' : root, path);
|
||||||
|
|
||||||
if (!await exists(file)) {
|
if (!await exists(file)) {
|
||||||
writeTextFile(file, JSON.stringify({
|
if (await dirname(file) !== root) {
|
||||||
|
await createDir(await dirname(file), { recursive: true });
|
||||||
|
}
|
||||||
|
await writeTextFile(file, isList ? '[]' : JSON.stringify({
|
||||||
name: 'ChatGPT',
|
name: 'ChatGPT',
|
||||||
link: 'https://github.com/lencx/ChatGPT/blob/main/chat.model.md',
|
link: 'https://github.com/lencx/ChatGPT',
|
||||||
data: null,
|
|
||||||
...defaultVal,
|
...defaultVal,
|
||||||
}, null, 2))
|
}, null, 2))
|
||||||
}
|
}
|
||||||
@@ -36,8 +48,19 @@ export const readJSON = async (path: string, defaultVal = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const writeJSON = async (path: string, data: Record<string, any>) => {
|
type writeJSONOpts = { dir?: string, isRoot?: boolean };
|
||||||
|
export const writeJSON = async (path: string, data: Record<string, any>, opts: writeJSONOpts = {}) => {
|
||||||
|
const { isRoot = false } = opts;
|
||||||
const root = await chatRoot();
|
const root = await chatRoot();
|
||||||
const file = await join(root, path);
|
const file = await join(isRoot ? '' : root, path);
|
||||||
|
|
||||||
|
if (isRoot && !await exists(await dirname(file))) {
|
||||||
|
await createDir(await dirname(file), { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
await writeTextFile(file, JSON.stringify(data, null, 2));
|
await writeTextFile(file, JSON.stringify(data, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fmtDate = (date: any) => dayjs(date).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
|
||||||
|
export const genCmd = (act: string) => act.replace(/\s+|\/+/g, '_').replace(/[^\d\w]/g, '').toLocaleLowerCase();
|
||||||
39
src/view/LanguageModel/index.scss
vendored
39
src/view/LanguageModel/index.scss
vendored
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
95
src/view/LanguageModel/index.tsx
vendored
95
src/view/LanguageModel/index.tsx
vendored
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
112
src/view/model/SyncCustom/Form.tsx
vendored
Normal file
112
src/view/model/SyncCustom/Form.tsx
vendored
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
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;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initFormValue = {
|
||||||
|
act: '',
|
||||||
|
enable: true,
|
||||||
|
tags: [],
|
||||||
|
prompt: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record, type }, ref) => {
|
||||||
|
const isDisabled = type === 'edit';
|
||||||
|
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 disabled={isDisabled}>
|
||||||
|
<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 disabled={isDisabled}>
|
||||||
|
<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);
|
||||||
89
src/view/model/SyncCustom/config.tsx
vendored
Normal file
89
src/view/model/SyncCustom/config.tsx
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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>
|
||||||
|
<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>
|
||||||
|
<Popconfirm
|
||||||
|
overlayStyle={{ width: 250 }}
|
||||||
|
title="Sync will overwrite the previous data, confirm to sync?"
|
||||||
|
onConfirm={() => actions.setRecord(row, 'sync')}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<a>Sync</a>
|
||||||
|
</Popconfirm>
|
||||||
|
{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
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="Sync PATH"
|
||||||
|
onOk={handleOk}
|
||||||
|
destroyOnClose
|
||||||
|
maskClosable={false}
|
||||||
|
>
|
||||||
|
<SyncForm ref={formRef} record={opInfo?.opRecord} type={opInfo.opType} />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/view/model/SyncPrompts/config.tsx
vendored
Normal file
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
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/view/model/SyncPrompts/index.tsx
vendored
Normal file
110
src/view/model/SyncPrompts/index.tsx
vendored
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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">
|
||||||
|
<Popconfirm
|
||||||
|
overlayStyle={{ width: 250 }}
|
||||||
|
title="Sync will overwrite the previous data, confirm to sync?"
|
||||||
|
placement="topLeft"
|
||||||
|
onConfirm={handleSync}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<Button type="primary">Sync</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
<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(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
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">/{row.cmd ? row.cmd : 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
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import type { FormProps } from 'antd';
|
|||||||
import Tags from '@comps/Tags';
|
import Tags from '@comps/Tags';
|
||||||
import { DISABLE_AUTO_COMPLETE } from '@/utils';
|
import { DISABLE_AUTO_COMPLETE } from '@/utils';
|
||||||
|
|
||||||
interface LanguageModelProps {
|
interface UserCustomFormProps {
|
||||||
record?: Record<string|symbol, any> | null;
|
record?: Record<string|symbol, any> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ const initFormValue = {
|
|||||||
prompt: '',
|
prompt: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LanguageModel: ForwardRefRenderFunction<FormProps, LanguageModelProps> = ({ record }, ref) => {
|
const UserCustomForm: ForwardRefRenderFunction<FormProps, UserCustomFormProps> = ({ record }, ref) => {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
useImperativeHandle(ref, () => ({ form }));
|
useImperativeHandle(ref, () => ({ form }));
|
||||||
|
|
||||||
@@ -63,4 +63,4 @@ const LanguageModel: ForwardRefRenderFunction<FormProps, LanguageModelProps> = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default forwardRef(LanguageModel);
|
export default forwardRef(UserCustomForm);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Tag, Switch, Tooltip, Space } from 'antd';
|
import { Tag, Switch, Tooltip, Space, Popconfirm } from 'antd';
|
||||||
|
|
||||||
export const modelColumns = () => [
|
export const modelColumns = () => [
|
||||||
{
|
{
|
||||||
@@ -29,7 +29,9 @@ export const modelColumns = () => [
|
|||||||
dataIndex: 'enable',
|
dataIndex: 'enable',
|
||||||
key: 'enable',
|
key: 'enable',
|
||||||
width: 80,
|
width: 80,
|
||||||
render: (v: boolean = false) => <Switch checked={v} disabled />,
|
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',
|
title: 'Prompt',
|
||||||
@@ -48,7 +50,14 @@ export const modelColumns = () => [
|
|||||||
render: (_: any, row: any, actions: any) => (
|
render: (_: any, row: any, actions: any) => (
|
||||||
<Space size="middle">
|
<Space size="middle">
|
||||||
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
|
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
|
||||||
<a onClick={() => actions.setRecord(row, 'delete')}>Delete</a>
|
<Popconfirm
|
||||||
|
title="Are you sure to delete this model?"
|
||||||
|
onConfirm={() => actions.setRecord(row, 'delete')}
|
||||||
|
okText="Yes"
|
||||||
|
cancelText="No"
|
||||||
|
>
|
||||||
|
<a>Delete</a>
|
||||||
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
139
src/view/model/UserCustom/index.tsx
vendored
Normal file
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user