1
0
mirror of https://github.com/lencx/ChatGPT.git synced 2024-10-01 01:06:13 -04:00

Merge pull request #38 from lencx/dev

This commit is contained in:
lencx 2022-12-17 18:12:09 +08:00 committed by GitHub
commit 91cebe82db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1018 additions and 132 deletions

3
.gitattributes vendored
View File

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

View File

@ -22,9 +22,9 @@
**最新版:** **最新版:**
- `Mac`: [ChatGPT_0.3.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.3.0/ChatGPT_0.3.0_x64.dmg) - `Mac`: [ChatGPT_0.4.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.0_x64.dmg)
- `Linux`: [chat-gpt_0.3.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.3.0/chat-gpt_0.3.0_amd64.deb) - `Linux`: [chat-gpt_0.4.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/chat-gpt_0.4.0_amd64.deb)
- `Windows`: [ChatGPT_0.3.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.3.0/ChatGPT_0.3.0_x64_en-US.msi) - `Windows`: [ChatGPT_0.4.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.0_x64_en-US.msi)
[其他版本...](https://github.com/lencx/ChatGPT/releases) [其他版本...](https://github.com/lencx/ChatGPT/releases)
@ -47,6 +47,25 @@ tap repo, "https://github.com/#{repo}.git"
cask "popcorn-time", args: { "no-quarantine": true } cask "popcorn-time", args: { "no-quarantine": true }
~~~ ~~~
## 📢 公告
这是一个令人兴奋的重大更新。像 `Telegram 机器人指令` 那样工作,帮助你快速填充自定模型,来让 ChatGPT 按照你想要的方式去工作。这个项目倾注了我大量业余时间,如果它对你有所帮助,宣传转发,或者 star 都是对我的巨大鼓励。我希望我可以持续更新下去,加入更多有趣的功能。
### 如何使用指令?
你可以从 [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) 来寻找有趣的功能来导入到应用。
![chat cmd](./assets/chat-cmd-1.png)
![chat cmd](./assets/chat-cmd-2.png)
数据导入完成后,可以重新启动应用来使配置生效(`Menu -> Preferences -> Restart ChatGPT`)。
项目会维护一份常用命令,您也可以直接将 [chat.model.json](https://github.com/lencx/ChatGPT/blob/main/chat.model.json) 复制到你的本地目录 `~/.chatgpt/chat.model.json`
在 ChatGPT 文本输入区域,键入 `/` 开头的字符,则会弹出指令提示,按下空格键,它会默认将命令关联的文本填充到输入区域(注意:如果包含多个指令提示,它只会选择第一个作为填充,你可以持续输入,直到第一个提示命令为你想要时,再按下空格键。或者使用鼠标来点击多条指令中的某一个)。填充完成后,你只需要按下回车键即可。
![chatgpt](assets/chatgpt.gif)
## ✨ 功能概览 ## ✨ 功能概览
- 跨平台: `macOS` `Linux` `Windows` - 跨平台: `macOS` `Linux` `Windows`
@ -60,11 +79,13 @@ cask "popcorn-time", args: { "no-quarantine": true }
- **Preferences (喜好)** - **Preferences (喜好)**
- `Theme` - `Light`, `Dark` (仅支持 macOS 和 Windows) - `Theme` - `Light`, `Dark` (仅支持 macOS 和 Windows)
- `Always On Top`: 窗口置顶 - `Stay On Top`: 窗口置顶
- `Titlebar`: 是否显示 `Titlebar`,仅 macOS 支持 - `Titlebar`: 是否显示 `Titlebar`,仅 macOS 支持
- `Inject Script`: 用于修改网站的用户自定义脚本 - `Inject Script`: 用于修改网站的用户自定义脚本
- `Hide Dock Icon` ([#35](https://github.com/lencx/ChatGPT/issues/35)): 隐藏 Dock 中的应用图标 (仅 macOS 支持)
- 系统图盘右键单击打开菜单,然后在菜单项中点击 `Show Dock Icon` 可以重新将应用图标显示在 Dock`SystemTrayMenu -> Show Dock Icon`
- `Control Center`: ChatGPT 应用的控制中心,它将为应用提供无限的可能 - `Control Center`: ChatGPT 应用的控制中心,它将为应用提供无限的可能
- 设置 `Theme``Always on Top``Titlebar` 等 - 设置 `Theme``Stay On Top``Titlebar` 等
- `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): 自定义 `user agent` 防止网站安全检测,默认值为空 - `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): 自定义 `user agent` 防止网站安全检测,默认值为空
- `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): 切换网站源地址,默认为 `https://chat.openai.com`。需要注意的是镜像网站的 UI 需要和原网站一致,否则可能会导致某些功能不工作 - `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): 切换网站源地址,默认为 `https://chat.openai.com`。需要注意的是镜像网站的 UI 需要和原网站一致,否则可能会导致某些功能不工作
- `Go to Config`: 打开 ChatGPT 配置目录 (`path: ~/.chatgpt/*`) - `Go to Config`: 打开 ChatGPT 配置目录 (`path: ~/.chatgpt/*`)
@ -141,6 +162,7 @@ yarn build
## ❤️ 感谢 ## ❤️ 感谢
- 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改 - 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改
- 感谢 [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) 项目为这个应用自定义指令功能所带来的启发
--- ---

View File

@ -23,9 +23,9 @@
**Latest:** **Latest:**
- `Mac`: [ChatGPT_0.3.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.3.0/ChatGPT_0.3.0_x64.dmg) - `Mac`: [ChatGPT_0.4.0_x64.dmg](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/ChatGPT_0.4.0_x64.dmg)
- `Linux`: [chat-gpt_0.3.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.3.0/chat-gpt_0.3.0_amd64.deb) - `Linux`: [chat-gpt_0.4.0_amd64.deb](https://github.com/lencx/ChatGPT/releases/download/v0.4.0/chat-gpt_0.4.0_amd64.deb)
- `Windows`: [ChatGPT_0.3.0_x64_en-US.msi](https://github.com/lencx/ChatGPT/releases/download/v0.3.0/ChatGPT_0.3.0_x64_en-US.msi) - `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)
[Other version...](https://github.com/lencx/ChatGPT/releases) [Other version...](https://github.com/lencx/ChatGPT/releases)
@ -48,6 +48,25 @@ tap repo, "https://github.com/#{repo}.git"
cask "popcorn-time", args: { "no-quarantine": true } cask "popcorn-time", args: { "no-quarantine": true }
~~~ ~~~
## 📢 Announcement
This is a major and exciting update. It works like a `Telegram bot command` and helps you quickly populate custom models to make chatgpt work the way you want it to. This project has taken a lot of my spare time, so if it helps you, please help spread the word or star it would be a great encouragement to me. I hope I can keep updating it and adding more interesting features.
### How does it work?
You can look at [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts) to find interesting features to import into the app.
![chat cmd](./assets/chat-cmd-1.png)
![chat cmd](./assets/chat-cmd-2.png)
After the data import is done, you can restart the app to make the configuration take effect (`Menu -> Preferences -> Restart ChatGPT`).
The project maintains a list of common commands, or you can copy [chat.model.json](https://github.com/lencx/ChatGPT/blob/main/chat.model.json) directly to your local directory `~/.chatgpt/chat.model.json`
In the chatgpt text input area, type a character starting with `/` to bring up the command prompt, press the spacebar, and it will fill the input area with the text associated with the command by default (note: if it contains multiple command prompts, it will only select the first one as the fill, you can keep typing until the first prompted command is the one you want, then press the spacebar. Or use the mouse to click on one of the multiple commands). When the fill is complete, you simply press the Enter key.
![chatgpt](assets/chatgpt.gif)
## ✨ Features ## ✨ Features
- Multi-platform: `macOS` `Linux` `Windows` - Multi-platform: `macOS` `Linux` `Windows`
@ -61,11 +80,13 @@ cask "popcorn-time", args: { "no-quarantine": true }
- **Preferences** - **Preferences**
- `Theme` - `Light`, `Dark` (Only macOS and Windows are supported). - `Theme` - `Light`, `Dark` (Only macOS and Windows are supported).
- `Always on Top`: The window is always on top of other windows. - `Stay On Top`: The window is stay on top of other windows.
- `Titlebar`: Whether to display the titlebar, supported by macOS only. - `Titlebar`: Whether to display the titlebar, supported by macOS only.
- `Hide Dock Icon` ([#35](https://github.com/lencx/ChatGPT/issues/35)): Hide application icons from the Dock(support macOS only).
- Right-click on the SystemTray to open the menu, then click `Show Dock Icon` in the menu item to re-display the application icon in the Dock (`SystemTrayMenu -> Show Dock Icon`).
- `Inject Script`: Using scripts to modify pages. - `Inject Script`: Using scripts to modify pages.
- `Control Center`: The control center of ChatGPT application, it will give unlimited imagination to the application. - `Control Center`: The control center of ChatGPT application, it will give unlimited imagination to the application.
- `Theme`, `Always on Top`, `Titlebar`, ... - `Theme`, `Stay On Top`, `Titlebar`, ...
- `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): Custom `user agent`, which may be required in some scenarios. The default value is the empty string. - `User Agent` ([#17](https://github.com/lencx/ChatGPT/issues/17)): Custom `user agent`, which may be required in some scenarios. The default value is the empty string.
- `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): Switch the site source address, the default is `https://chat.openai.com`, please make sure the mirror site UI is the same as the original address. Otherwise, some functions may not be available. - `Switch Origin` ([#14](https://github.com/lencx/ChatGPT/issues/14)): Switch the site source address, the default is `https://chat.openai.com`, please make sure the mirror site UI is the same as the original address. Otherwise, some functions may not be available.
- `Go to Config`: Open the configuration file directory (`path: ~/.chatgpt/*`). - `Go to Config`: Open the configuration file directory (`path: ~/.chatgpt/*`).
@ -145,6 +166,7 @@ yarn build
## ❤️ Thanks ## ❤️ Thanks
- The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications. - The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications.
- Thanks to the [Awesome ChatGPT Prompts](https://github.com/f/awesome-chatgpt-prompts) repository for inspiring the custom command function for this application.
--- ---

View File

@ -1,5 +1,11 @@
# UPDATE LOG # UPDATE LOG
## v0.4.0
feat:
- customize the ChatGPT prompts command (https://github.com/lencx/ChatGPT#-announcement)
- menu enhancement: hide application icons from the Dock (support macOS only)
## v0.3.0 ## v0.3.0
fix: can't open ChatGPT fix: can't open ChatGPT
@ -36,7 +42,7 @@ feat: tray window
## v0.1.6 ## v0.1.6
feat: feat:
- always on top - stay on top
- export ChatGPT history - export ChatGPT history
## v0.1.5 ## v0.1.5

BIN
assets/chat-cmd-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
assets/chat-cmd-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/chatgpt.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 MiB

82
chat.model.json Normal file
View File

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

3
chat.model.md Normal file
View File

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

View File

@ -36,7 +36,8 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.4.5" "react-router-dom": "^6.4.5",
"uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^1.2.2", "@tauri-apps/cli": "^1.2.2",
@ -45,6 +46,7 @@
"@types/node": "^18.7.10", "@types/node": "^18.7.10",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/uuid": "^9.0.0",
"@vitejs/plugin-react": "^3.0.0", "@vitejs/plugin-react": "^3.0.0",
"sass": "^1.56.2", "sass": "^1.56.2",
"typescript": "^4.9.4", "typescript": "^4.9.4",

2
scripts/download.js vendored
View File

@ -25,7 +25,7 @@ async function rewrite(filename) {
async function init() { async function init() {
rewrite('README.md'); rewrite('README.md');
rewrite('README-ZH.md'); rewrite('README-ZH_CN.md');
} }
init().catch(console.error); init().catch(console.error);

View File

@ -1,5 +1,5 @@
use crate::{conf::ChatConfJson, utils}; use crate::{conf::ChatConfJson, utils};
use std::fs; use std::{fs, path::PathBuf};
use tauri::{api, command, AppHandle, Manager}; use tauri::{api, command, AppHandle, Manager};
#[command] #[command]
@ -59,3 +59,15 @@ pub fn form_msg(app: AppHandle, label: &str, title: &str, msg: &str) {
let win = app.app_handle().get_window(label); let win = app.app_handle().get_window(label);
tauri::api::dialog::message(win.as_ref(), title, msg); tauri::api::dialog::message(win.as_ref(), title, msg);
} }
#[command]
pub fn open_file(path: PathBuf) {
utils::open_file(path);
}
#[command]
pub fn get_chat_model() -> serde_json::Value {
let path = utils::chat_root().join("chat.model.json");
let content = fs::read_to_string(path).unwrap_or_else(|_| r#"{"data":[]}"#.to_string());
serde_json::from_str(&content).unwrap()
}

View File

@ -3,15 +3,15 @@ use crate::{
utils, utils,
}; };
use tauri::{ use tauri::{
utils::assets::EmbeddedAssets, AboutMetadata, AppHandle, Context, CustomMenuItem, Manager, AboutMetadata, AppHandle, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray,
Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowMenuEvent, SystemTrayEvent, SystemTrayMenu, WindowMenuEvent, SystemTrayMenuItem,
}; };
use tauri_plugin_positioner::{on_tray_event, Position, WindowExt}; use tauri_plugin_positioner::{on_tray_event, Position, WindowExt};
// --- Menu // --- Menu
pub fn init(context: &Context<EmbeddedAssets>) -> Menu { pub fn init() -> Menu {
let chat_conf = ChatConfJson::get_chat_conf(); let chat_conf = ChatConfJson::get_chat_conf();
let name = &context.package_info().name; let name = "ChatGPT";
let app_menu = Submenu::new( let app_menu = Submenu::new(
name, name,
Menu::new() Menu::new()
@ -25,18 +25,18 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
.add_native_item(MenuItem::Quit), .add_native_item(MenuItem::Quit),
); );
let always_on_top = CustomMenuItem::new("always_on_top".to_string(), "Always on Top") let stay_on_top =
.accelerator("CmdOrCtrl+T"); CustomMenuItem::new("stay_on_top".to_string(), "Stay On Top").accelerator("CmdOrCtrl+T");
let titlebar = let titlebar =
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B"); CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light"); let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark"); let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
let is_dark = chat_conf.theme == "Dark"; let is_dark = chat_conf.theme == "Dark";
let always_on_top_menu = if chat_conf.always_on_top { let stay_on_top_menu = if chat_conf.stay_on_top {
always_on_top.selected() stay_on_top.selected()
} else { } else {
always_on_top stay_on_top
}; };
let titlebar_menu = if chat_conf.titlebar { let titlebar_menu = if chat_conf.titlebar {
titlebar.selected() titlebar.selected()
@ -62,9 +62,11 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
}), }),
) )
.into(), .into(),
always_on_top_menu.into(), stay_on_top_menu.into(),
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
titlebar_menu.into(), titlebar_menu.into(),
#[cfg(target_os = "macos")]
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
MenuItem::Separator.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")
@ -119,7 +121,6 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen") CustomMenuItem::new("scroll_bottom".to_string(), "Scroll to Bottom of Screen")
.accelerator("CmdOrCtrl+Down"), .accelerator("CmdOrCtrl+Down"),
) )
.add_native_item(MenuItem::Zoom)
.add_native_item(MenuItem::Separator) .add_native_item(MenuItem::Separator)
.add_item( .add_item(
CustomMenuItem::new("reload".to_string(), "Refresh the Screen") CustomMenuItem::new("reload".to_string(), "Refresh the Screen")
@ -127,6 +128,13 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
), ),
); );
let window_menu = Submenu::new(
"Window",
Menu::new()
.add_native_item(MenuItem::Minimize)
.add_native_item(MenuItem::Zoom),
);
let help_menu = Submenu::new( let help_menu = Submenu::new(
"Help", "Help",
Menu::new() Menu::new()
@ -143,6 +151,7 @@ pub fn init(context: &Context<EmbeddedAssets>) -> Menu {
.add_submenu(preferences_menu) .add_submenu(preferences_menu)
.add_submenu(edit_menu) .add_submenu(edit_menu)
.add_submenu(view_menu) .add_submenu(view_menu)
.add_submenu(window_menu)
.add_submenu(help_menu) .add_submenu(help_menu)
} }
@ -165,6 +174,9 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
"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()),
"hide_dock_icon" => {
ChatConfJson::amend(&serde_json::json!({ "hide_dock_icon": true }), Some(app)).unwrap()
}
"titlebar" => { "titlebar" => {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let chat_conf = conf::ChatConfJson::get_chat_conf();
ChatConfJson::amend( ChatConfJson::amend(
@ -182,19 +194,15 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
}; };
ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap(); ChatConfJson::amend(&serde_json::json!({ "theme": theme }), Some(app)).unwrap();
} }
"always_on_top" => { "stay_on_top" => {
let mut always_on_top = state.always_on_top.lock().unwrap(); let mut stay_on_top = state.stay_on_top.lock().unwrap();
*always_on_top = !*always_on_top; *stay_on_top = !*stay_on_top;
menu_handle menu_handle
.get_item(menu_id) .get_item(menu_id)
.set_selected(*always_on_top) .set_selected(*stay_on_top)
.unwrap(); .unwrap();
win.set_always_on_top(*always_on_top).unwrap(); win.set_always_on_top(*stay_on_top).unwrap();
ChatConfJson::amend( ChatConfJson::amend(&serde_json::json!({ "stay_on_top": *stay_on_top }), None).unwrap();
&serde_json::json!({ "always_on_top": *always_on_top }),
None,
)
.unwrap();
} }
// View // View
"reload" => win.eval("window.location.reload()").unwrap(), "reload" => win.eval("window.location.reload()").unwrap(),
@ -230,24 +238,64 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
// --- SystemTray Menu // --- SystemTray Menu
pub fn tray_menu() -> SystemTray { pub fn tray_menu() -> SystemTray {
SystemTray::new().with_menu(SystemTrayMenu::new()) SystemTray::new().with_menu(
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("control_center".to_string(), "Control Center"))
.add_item(CustomMenuItem::new("show_dock_icon".to_string(), "Show Dock Icon"))
.add_item(CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit".to_string(), "Quit ChatGPT"))
)
} }
// --- SystemTray Event // --- SystemTray Event
pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) { pub fn tray_handler(handle: &AppHandle, event: SystemTrayEvent) {
let core_win = handle.get_window("core").unwrap();
on_tray_event(handle, &event); on_tray_event(handle, &event);
if let SystemTrayEvent::LeftClick { .. } = event { let app = handle.clone();
core_win.minimize().unwrap();
let mini_win = handle.get_window("mini").unwrap();
mini_win.move_window(Position::TrayCenter).unwrap();
if mini_win.is_visible().unwrap() { match event {
mini_win.hide().unwrap(); SystemTrayEvent::LeftClick { .. } => {
} else { let chat_conf = conf::ChatConfJson::get_chat_conf();
mini_win.show().unwrap();
if !chat_conf.hide_dock_icon {
let core_win = handle.get_window("core").unwrap();
core_win.minimize().unwrap();
}
let tray_win = handle.get_window("tray").unwrap();
tray_win.move_window(Position::TrayCenter).unwrap();
if tray_win.is_visible().unwrap() {
tray_win.hide().unwrap();
} else {
tray_win.show().unwrap();
}
} }
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"control_center" => app.get_window("main").unwrap().show().unwrap(),
"restart" => tauri::api::process::restart(&handle.env()),
"show_dock_icon" => {
ChatConfJson::amend(
&serde_json::json!({ "hide_dock_icon": false }),
Some(app),
)
.unwrap();
},
"hide_dock_icon" => {
let chat_conf = conf::ChatConfJson::get_chat_conf();
if !chat_conf.hide_dock_icon {
ChatConfJson::amend(
&serde_json::json!({ "hide_dock_icon": true }),
Some(app),
)
.unwrap();
}
},
"quit" => std::process::exit(0),
_ => (),
}
_ => (),
} }
} }

View File

@ -5,40 +5,55 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
let chat_conf = ChatConfJson::get_chat_conf(); let chat_conf = ChatConfJson::get_chat_conf();
let url = chat_conf.origin.to_string(); let url = chat_conf.origin.to_string();
let theme = ChatConfJson::theme(); let theme = ChatConfJson::theme();
window::mini_window(&app.app_handle()); let handle = app.app_handle();
#[cfg(target_os = "macos")] std::thread::spawn(move || {
WindowBuilder::new(app, "core", WindowUrl::App(url.into())) window::tray_window(&handle);
.resizable(true) });
.fullscreen(false)
.inner_size(800.0, 600.0)
.hidden_title(true)
.theme(theme)
.always_on_top(chat_conf.always_on_top)
.title_bar_style(ChatConfJson::titlebar())
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../assets/html2canvas.js"))
.initialization_script(include_str!("../assets/jspdf.js"))
.initialization_script(include_str!("../assets/core.js"))
.initialization_script(include_str!("../assets/export.js"))
.user_agent(&chat_conf.ua_window)
.build()?;
#[cfg(not(target_os = "macos"))] if chat_conf.hide_dock_icon {
WindowBuilder::new(app, "core", WindowUrl::App(url.into())) #[cfg(target_os = "macos")]
.title("ChatGPT") app.set_activation_policy(tauri::ActivationPolicy::Accessory);
.resizable(true) } else {
.fullscreen(false) let app = app.handle();
.inner_size(800.0, 600.0) std::thread::spawn(move || {
.theme(theme) #[cfg(target_os = "macos")]
.always_on_top(chat_conf.always_on_top) WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
.initialization_script(&utils::user_script()) .title("ChatGPT")
.initialization_script(include_str!("../assets/html2canvas.js")) .resizable(true)
.initialization_script(include_str!("../assets/jspdf.js")) .fullscreen(false)
.initialization_script(include_str!("../assets/core.js")) .inner_size(800.0, 600.0)
.initialization_script(include_str!("../assets/export.js")) .hidden_title(true)
.user_agent(&chat_conf.ua_window) .theme(theme)
.build()?; .always_on_top(chat_conf.stay_on_top)
.title_bar_style(ChatConfJson::titlebar())
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../assets/html2canvas.js"))
.initialization_script(include_str!("../assets/jspdf.js"))
.initialization_script(include_str!("../assets/core.js"))
.initialization_script(include_str!("../assets/export.js"))
.initialization_script(include_str!("../assets/cmd.js"))
.user_agent(&chat_conf.ua_window)
.build().unwrap();
#[cfg(not(target_os = "macos"))]
WindowBuilder::new(&app, "core", WindowUrl::App(url.into()))
.title("ChatGPT")
.resizable(true)
.fullscreen(false)
.inner_size(800.0, 600.0)
.theme(theme)
.always_on_top(chat_conf.stay_on_top)
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../assets/html2canvas.js"))
.initialization_script(include_str!("../assets/jspdf.js"))
.initialization_script(include_str!("../assets/core.js"))
.initialization_script(include_str!("../assets/export.js"))
.initialization_script(include_str!("../assets/cmd.js"))
.user_agent(&chat_conf.ua_window)
.build().unwrap();
});
}
Ok(()) Ok(())
} }

View File

@ -1,25 +1,30 @@
use crate::{conf, utils}; use crate::{conf, utils};
use tauri::{utils::config::WindowUrl, window::WindowBuilder}; use tauri::{utils::config::WindowUrl, window::WindowBuilder};
pub fn mini_window(handle: &tauri::AppHandle) { pub fn tray_window(handle: &tauri::AppHandle) {
let chat_conf = conf::ChatConfJson::get_chat_conf(); let chat_conf = conf::ChatConfJson::get_chat_conf();
let theme = conf::ChatConfJson::theme(); let theme = conf::ChatConfJson::theme();
let app = handle.clone();
WindowBuilder::new(handle, "mini", WindowUrl::App(chat_conf.origin.into())) std::thread::spawn(move || {
.resizable(false) WindowBuilder::new(&app, "tray", WindowUrl::App(chat_conf.origin.into()))
.fullscreen(false) .title("ChatGPT")
.inner_size(360.0, 540.0) .resizable(false)
.decorations(false) .fullscreen(false)
.always_on_top(true) .inner_size(360.0, 540.0)
.theme(theme) .decorations(false)
.initialization_script(&utils::user_script()) .always_on_top(true)
.initialization_script(include_str!("../assets/html2canvas.js")) .theme(theme)
.initialization_script(include_str!("../assets/jspdf.js")) .initialization_script(&utils::user_script())
.initialization_script(include_str!("../assets/core.js")) .initialization_script(include_str!("../assets/html2canvas.js"))
.initialization_script(include_str!("../assets/export.js")) .initialization_script(include_str!("../assets/jspdf.js"))
.user_agent(&chat_conf.ua_tray) .initialization_script(include_str!("../assets/core.js"))
.build() .initialization_script(include_str!("../assets/export.js"))
.unwrap() .initialization_script(include_str!("../assets/cmd.js"))
.hide() .user_agent(&chat_conf.ua_tray)
.unwrap(); .build()
.unwrap()
.hide()
.unwrap();
});
} }

150
src-tauri/src/assets/cmd.js vendored Normal file
View File

@ -0,0 +1,150 @@
// *** Core Script - CMD ***
function init() {
const styleDom = document.createElement('style');
styleDom.innerHTML = `form {
position: relative;
}
.chat-model-cmd-list {
position: absolute;
width: 400px;
bottom: 60px;
max-height: 100px;
overflow: auto;
z-index: 9999;
}
.chat-model-cmd-list>div {
border: solid 2px #d8d8d8;
border-radius: 5px;
background-color: #fff;
}
.chat-model-cmd-list .cmd-item {
font-size: 12px;
border-bottom: solid 1px #888;
padding: 2px 4px;
display: flex;
user-select: none;
cursor: pointer;
}
.chat-model-cmd-list .cmd-item:last-child {
border-bottom: none;
}
.chat-model-cmd-list .cmd-item b {
display: inline-block;
width: 120px;
border-radius: 4px;
margin-right: 10px;
color: #2a2a2a;
}
.chat-model-cmd-list .cmd-item i {
width: 270px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
color: #888;
}`;
document.head.append(styleDom);
if (window.formInterval) {
clearInterval(window.formInterval);
}
window.formInterval = setInterval(() => {
const form = document.querySelector("form");
if (!form) return;
clearInterval(window.formInterval);
cmdTip();
}, 200);
}
async function cmdTip() {
const chatModelJson = await invoke('get_chat_model') || {};
if (!chatModelJson.data && chatModelJson.data.length <= 0) return;
const data = chatModelJson.data || [];
const modelDom = document.createElement('div');
modelDom.classList.add('chat-model-cmd-list');
document.querySelector('form').appendChild(modelDom);
const itemDom = (v) => `<div class="cmd-item" data-prompt="${encodeURIComponent(v.prompt)}"><b>/${v.cmd}</b><i>${v.act}</i></div>`;
const searchInput = document.querySelector('form textarea');
// Enter a command starting with `/` and press a space to automatically fill `chatgpt prompt`.
// If more than one command appears in the search results, the first one will be used by default.
searchInput.addEventListener('keydown', (event) => {
if (!window.__CHAT_MODEL_CMD__) {
return;
}
if (event.keyCode === 32) {
searchInput.value = window.__CHAT_MODEL_CMD__;
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD__;
}
if (event.keyCode === 13) {
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD__;
}
});
searchInput.addEventListener('input', (event) => {
const query = searchInput.value;
if (!query || !/^\//.test(query)) {
modelDom.innerHTML = '';
return;
}
// all cmd result
if (query === '/') {
const result = data.filter(i => i.enable);
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
window.__CHAT_MODEL_CMD__ = result[0]?.prompt.trim();
return;
}
const result = data.filter(i => i.enable && new RegExp(query.substring(1)).test(i.cmd));
if (result.length > 0) {
modelDom.innerHTML = `<div>${result.map(itemDom).join('')}</div>`;
window.__CHAT_MODEL_CMD__ = result[0]?.prompt.trim();
} else {
modelDom.innerHTML = '';
delete window.__CHAT_MODEL_CMD__;
}
}, {
capture: false,
passive: true,
once: false
});
if (window.searchInterval) {
clearInterval(window.searchInterval);
}
window.searchInterval = setInterval(() => {
// The `chatgpt prompt` fill can be done by clicking on the event.
const searchDom = document.querySelector("form .chat-model-cmd-list>div");
if (!searchDom) return;
searchDom.addEventListener('click', (event) => {
// .cmd-item
const item = event.target.closest("div");
if (item) {
const val = decodeURIComponent(item.getAttribute('data-prompt'));
searchInput.value = val;
document.querySelector('form textarea').focus();
window.__CHAT_MODEL_CMD__ = val;
modelDom.innerHTML = '';
}
}, {
capture: false,
passive: true,
once: false
});
}, 200);
}
if (
document.readyState === "complete" ||
document.readyState === "interactive"
) {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}

View File

@ -41,7 +41,7 @@ window.invoke = invoke;
window.transformCallback = transformCallback; window.transformCallback = transformCallback;
async function init() { async function init() {
if (__TAURI_METADATA__.__currentWindow.label === 'mini') { if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
document.getElementsByTagName('html')[0].style['font-size'] = '70%'; document.getElementsByTagName('html')[0].style['font-size'] = '70%';
} }

View File

@ -14,18 +14,20 @@ pub const ISSUES_URL: &str = "https://github.com/lencx/ChatGPT/issues";
pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md"; pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md";
pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md"; pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md";
pub const DEFAULT_CHAT_CONF: &str = r#"{ pub const DEFAULT_CHAT_CONF: &str = r#"{
"always_on_top": false, "stay_on_top": false,
"theme": "Light", "theme": "Light",
"titlebar": true, "titlebar": true,
"hide_dock_icon": false,
"default_origin": "https://chat.openai.com", "default_origin": "https://chat.openai.com",
"origin": "https://chat.openai.com", "origin": "https://chat.openai.com",
"ua_window": "", "ua_window": "",
"ua_tray": "" "ua_tray": ""
}"#; }"#;
pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
"always_on_top": false, "stay_on_top": false,
"theme": "Light", "theme": "Light",
"titlebar": false, "titlebar": false,
"hide_dock_icon": false,
"default_origin": "https://chat.openai.com", "default_origin": "https://chat.openai.com",
"origin": "https://chat.openai.com", "origin": "https://chat.openai.com",
"ua_window": "", "ua_window": "",
@ -33,22 +35,27 @@ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
}"#; }"#;
pub struct ChatState { pub struct ChatState {
pub always_on_top: Mutex<bool>, pub stay_on_top: Mutex<bool>,
} }
impl ChatState { impl ChatState {
pub fn default(chat_conf: ChatConfJson) -> Self { pub fn default(chat_conf: ChatConfJson) -> Self {
ChatState { ChatState {
always_on_top: Mutex::new(chat_conf.always_on_top), stay_on_top: Mutex::new(chat_conf.stay_on_top),
} }
} }
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct ChatConfJson { pub struct ChatConfJson {
// support macOS only
pub titlebar: bool, pub titlebar: bool,
pub always_on_top: bool, pub hide_dock_icon: bool,
// macOS and Windows
pub theme: String, pub theme: String,
pub stay_on_top: bool,
pub default_origin: String, pub default_origin: String,
pub origin: String, pub origin: String,
pub ua_window: String, pub ua_window: String,

View File

@ -26,10 +26,12 @@ fn main() {
cmd::form_cancel, cmd::form_cancel,
cmd::form_confirm, cmd::form_confirm,
cmd::form_msg, cmd::form_msg,
cmd::open_file,
cmd::get_chat_model,
]) ])
.setup(setup::init) .setup(setup::init)
.plugin(tauri_plugin_positioner::init()) .plugin(tauri_plugin_positioner::init())
.menu(menu::init(&context)) .menu(menu::init())
.system_tray(menu::tray_menu()) .system_tray(menu::tray_menu())
.on_menu_event(menu::menu_handler) .on_menu_event(menu::menu_handler)
.on_system_tray_event(menu::tray_handler) .on_system_tray_event(menu::tray_handler)

View File

@ -7,15 +7,22 @@
}, },
"package": { "package": {
"productName": "ChatGPT", "productName": "ChatGPT",
"version": "0.3.0" "version": "0.4.0"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
"all": true "all": true,
"fs": {
"all": true,
"scope": [
"$HOME/.chatgpt/*"
]
}
}, },
"systemTray": { "systemTray": {
"iconPath": "icons/tray-icon.png", "iconPath": "icons/tray-icon.png",
"iconAsTemplate": true "iconAsTemplate": true,
"menuOnLeftClick": false
}, },
"bundle": { "bundle": {
"active": true, "active": true,
@ -71,7 +78,9 @@
"title": "ChatGPT", "title": "ChatGPT",
"visible": false, "visible": false,
"width": 800, "width": 800,
"height": 600 "height": 600,
"minWidth": 800,
"minHeight": 600
} }
] ]
} }

98
src/components/Tags/index.tsx vendored Normal file
View File

@ -0,0 +1,98 @@
import { FC, useEffect, useRef, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Input, Tag } from 'antd';
import type { InputRef } from 'antd';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
interface TagsProps {
value?: string[];
onChange?: (v: string[]) => void;
}
const Tags: FC<TagsProps> = ({ value = [], onChange }) => {
const [tags, setTags] = useState<string[]>(value);
const [inputVisible, setInputVisible] = useState<boolean>(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<InputRef>(null);
useEffect(() => {
setTags(value);
}, [value])
useEffect(() => {
if (inputVisible) {
inputRef.current?.focus();
}
}, [inputVisible]);
const handleClose = (removedTag: string) => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
};
const showInput = () => {
setInputVisible(true);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && tags.indexOf(inputValue) === -1) {
const val = [...tags, inputValue];
setTags(val);
onChange && onChange(val);
}
setInputVisible(false);
setInputValue('');
};
const forMap = (tag: string) => {
const tagElem = (
<Tag
closable
onClose={(e) => {
e.preventDefault();
handleClose(tag);
}}
>
{tag}
</Tag>
);
return (
<span key={tag} style={{ display: 'inline-block' }}>
{tagElem}
</span>
);
};
const tagChild = tags.map(forMap);
return (
<>
<span style={{ marginBottom: 16 }}>{tagChild}</span>
{inputVisible && (
<Input
ref={inputRef}
type="text"
size="small"
style={{ width: 78 }}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
{...DISABLE_AUTO_COMPLETE}
/>
)}
{!inputVisible && (
<Tag onClick={showInput} className="chat-tag-new">
<PlusOutlined /> New Tag
</Tag>
)}
</>
);
};
export default Tags;

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

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

44
src/hooks/useColumns.ts vendored Normal file
View File

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

35
src/hooks/useData.ts vendored Normal file
View File

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

12
src/hooks/useInit.ts vendored Normal file
View File

@ -0,0 +1,12 @@
import { useRef, useEffect } from 'react';
// fix: Two interface requests will be made in development mode
export default function useInit(callback: () => void) {
const isInit = useRef(true);
useEffect(() => {
if (isInit.current) {
callback();
isInit.current = false;
}
}, [])
}

View File

@ -10,6 +10,7 @@
.chat-container { .chat-container {
padding: 20px; padding: 20px;
overflow: hidden;
} }
.ant-menu { .ant-menu {

View File

@ -1,9 +1,8 @@
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { Layout, Menu } from 'antd'; import { Layout, Menu } from 'antd';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import Routes, { menuItems } from '@/routes'; import Routes, { menuItems } from '@/routes';
import './index.scss'; import './index.scss';
const { Content, Footer, Sider } = Layout; const { Content, Footer, Sider } = Layout;
@ -14,13 +13,14 @@ interface ChatLayoutProps {
const ChatLayout: FC<ChatLayoutProps> = ({ children }) => { const ChatLayout: FC<ChatLayoutProps> = ({ children }) => {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const location = useLocation();
const go = useNavigate(); const go = useNavigate();
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: '100vh' }}>
<Sider theme="light" collapsible collapsed={collapsed} onCollapse={(value) => setCollapsed(value)}> <Sider theme="light" collapsible collapsed={collapsed} onCollapse={(value) => setCollapsed(value)}>
<div className="chat-logo"><img src="/logo.png" /></div> <div className="chat-logo"><img src="/logo.png" /></div>
<Menu defaultSelectedKeys={['/']} mode="vertical" items={menuItems} onClick={(i) => go(i.key)} /> <Menu defaultSelectedKeys={[location.pathname]} mode="vertical" items={menuItems} onClick={(i) => go(i.key)} />
</Sider> </Sider>
<Layout className="chat-layout"> <Layout className="chat-layout">
<Content className="chat-container"> <Content className="chat-container">

10
src/routes.tsx vendored
View File

@ -1,13 +1,13 @@
import { useRoutes } from 'react-router-dom'; import { useRoutes } from 'react-router-dom';
import { import {
DesktopOutlined, DesktopOutlined,
BulbOutlined BulbOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { RouteObject } from 'react-router-dom'; 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 ChatGPTPrompts from '@view/ChatGPTPrompts'; import LanguageModel from '@/view/LanguageModel';
export type ChatRouteObject = { export type ChatRouteObject = {
label: string; label: string;
@ -24,10 +24,10 @@ export const routes: Array<RouteObject & { meta: ChatRouteObject }> = [
}, },
}, },
{ {
path: '/chatgpt-prompts', path: '/language-model',
element: <ChatGPTPrompts />, element: <LanguageModel />,
meta: { meta: {
label: 'ChatGPT Prompts', label: 'Language Model',
icon: <BulbOutlined />, icon: <BulbOutlined />,
}, },
}, },

43
src/utils.ts vendored Normal file
View File

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

View File

@ -1,7 +0,0 @@
export default function Dashboard() {
return (
<div>
TODO: ChatGPT Prompts
</div>
)
}

16
src/view/General.tsx vendored
View File

@ -7,6 +7,8 @@ import { ask } from '@tauri-apps/api/dialog';
import { relaunch } from '@tauri-apps/api/process'; import { relaunch } from '@tauri-apps/api/process';
import { clone, omit, isEqual } from 'lodash'; import { clone, omit, isEqual } from 'lodash';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
const OriginLabel = ({ url }: { url: string }) => { const OriginLabel = ({ url }: { url: string }) => {
return ( return (
<span> <span>
@ -15,12 +17,6 @@ const OriginLabel = ({ url }: { url: string }) => {
) )
} }
const disableAuto = {
autoCapitalize: 'off',
autoComplete: 'off',
spellCheck: false
}
export default function General() { export default function General() {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [platformInfo, setPlatform] = useState<string>(''); const [platformInfo, setPlatform] = useState<string>('');
@ -72,7 +68,7 @@ export default function General() {
<Radio value="Dark">Dark</Radio> <Radio value="Dark">Dark</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item label="Always on Top" name="always_on_top" valuePropName="checked"> <Form.Item label="Stay On Top" name="stay_on_top" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
{platformInfo === 'darwin' && ( {platformInfo === 'darwin' && (
@ -81,13 +77,13 @@ export default function General() {
</Form.Item> </Form.Item>
)} )}
<Form.Item label={<OriginLabel url={chatConf?.default_origin} />} name="origin"> <Form.Item label={<OriginLabel url={chatConf?.default_origin} />} name="origin">
<Input placeholder="https://chat.openai.com" {...disableAuto} /> <Input placeholder="https://chat.openai.com" {...DISABLE_AUTO_COMPLETE} />
</Form.Item> </Form.Item>
<Form.Item label="User Agent (Window)" name="ua_window"> <Form.Item label="User Agent (Window)" name="ua_window">
<Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...disableAuto} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" /> <Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...DISABLE_AUTO_COMPLETE} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" />
</Form.Item> </Form.Item>
<Form.Item label="User Agent (SystemTray)" name="ua_tray"> <Form.Item label="User Agent (SystemTray)" name="ua_tray">
<Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...disableAuto} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" /> <Input.TextArea autoSize={{ minRows: 4, maxRows: 4 }} {...DISABLE_AUTO_COMPLETE} placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
<Space size={20}> <Space size={20}>

66
src/view/LanguageModel/Form.tsx vendored Normal file
View File

@ -0,0 +1,66 @@
import { useEffect, ForwardRefRenderFunction, useImperativeHandle, forwardRef } from 'react';
import { Form, Input, Switch } from 'antd';
import type { FormProps } from 'antd';
import Tags from '@comps/Tags';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
interface LanguageModelProps {
record?: Record<string|symbol, any> | null;
}
const initFormValue = {
act: '',
enable: true,
tags: [],
prompt: '',
};
const LanguageModel: ForwardRefRenderFunction<FormProps, LanguageModelProps> = ({ record }, ref) => {
const [form] = Form.useForm();
useImperativeHandle(ref, () => ({ form }));
useEffect(() => {
if (record) {
form.setFieldsValue(record);
}
}, [record]);
return (
<Form
form={form}
labelCol={{ span: 4 }}
initialValues={initFormValue}
>
<Form.Item
label="/{cmd}"
name="cmd"
rules={[{ required: true, message: 'Please input {cmd}!' }]}
>
<Input placeholder="Please input {cmd}" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
<Form.Item
label="Act"
name="act"
rules={[{ required: true, message: 'Please input act!' }]}
>
<Input placeholder="Please input act" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
<Form.Item label="Tags" name="tags">
<Tags value={record?.tags} />
</Form.Item>
<Form.Item label="Enable" name="enable" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item
label="Prompt"
name="prompt"
rules={[{ required: true, message: 'Please input prompt!' }]}
>
<Input.TextArea rows={4} placeholder="Please input prompt" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
</Form>
)
}
export default forwardRef(LanguageModel);

55
src/view/LanguageModel/config.tsx vendored Normal file
View File

@ -0,0 +1,55 @@
import { Tag, Switch, Tooltip, Space } from 'antd';
export const modelColumns = () => [
{
title: '/{cmd}',
dataIndex: 'cmd',
fixed: 'left',
width: 120,
key: 'cmd',
render: (v: string) => <Tag color="#2a2a2a">/{v}</Tag>
},
{
title: 'Act',
dataIndex: 'act',
key: 'act',
width: 200,
},
{
title: 'Tags',
dataIndex: 'tags',
key: 'tags',
width: 150,
render: (v: string[]) => (
<span className="chat-prompts-tags">{v?.map(i => <Tag key={i}>{i}</Tag>)}</span>
),
},
{
title: 'Enable',
dataIndex: 'enable',
key: 'enable',
width: 80,
render: (v: boolean = false) => <Switch checked={v} disabled />,
},
{
title: 'Prompt',
dataIndex: 'prompt',
key: 'prompt',
width: 300,
render: (v: string) => (
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
),
},
{
title: 'Action',
key: 'action',
fixed: 'right',
width: 120,
render: (_: any, row: any, actions: any) => (
<Space size="middle">
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
<a onClick={() => actions.setRecord(row, 'delete')}>Delete</a>
</Space>
),
}
];

39
src/view/LanguageModel/index.scss vendored Normal file
View File

@ -0,0 +1,39 @@
.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 Normal file
View File

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