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

chore: scripts

This commit is contained in:
lencx 2023-05-25 09:52:33 +08:00
parent 882593479b
commit ee0829d8db
39 changed files with 1370 additions and 1177 deletions

View File

@ -5,7 +5,7 @@
</p>
[![English badge](https://img.shields.io/badge/%E8%8B%B1%E6%96%87-English-blue)](./README.md)
[![简体中文 badge](https://img.shields.io/badge/%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-Simplified%20Chinese-blue)](./README-ZH_CN.md)
[![简体中文 badge](https://img.shields.io/badge/%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-Simplified%20Chinese-blue)](./README-ZH_CN.md)\
[![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases)
[![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr)
[![lencx](https://img.shields.io/badge/follow-lencx__-blue?style=flat&logo=Twitter)](https://twitter.com/lencx_)

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
# ChatGPT Scripts
ChatGPT Desktop Application Core Extension Scripts.
> ChatGPT Desktop Application Core Extension Scripts.
[ChatGPT/scripts](https://github.com/lencx/ChatGPT/tree/main/scripts)

10
scripts/chat.js vendored
View File

@ -4,7 +4,7 @@
* @url https://github.com/lencx/ChatGPT/tree/main/scripts/chat.js
*/
var chatInit = (() => {
function chatInit() {
const ICONS = {
copy: `<svg class="chatappico copy" stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>`,
cpok: `<svg class="chatappico cpok" viewBox="0 0 24 24"><g fill="none" stroke="#10a37f" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M8 4H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2M16 4h2a2 2 0 0 1 2 2v4m1 4H11"/><path d="m15 10l-4 4l4 4"/></g></svg>`,
@ -114,12 +114,10 @@ var chatInit = (() => {
currentIndex = -1;
};
}
return { init };
})();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
chatInit.init();
chatInit();
} else {
document.addEventListener('DOMContentLoaded', chatInit.init);
document.addEventListener('DOMContentLoaded', chatInit);
}

8
scripts/cmd.js vendored
View File

@ -4,7 +4,7 @@
* @url https://github.com/lencx/ChatGPT/tree/main/scripts/cmd.js
*/
function init() {
function cmdInit() {
const styleDom = document.createElement('style');
styleDom.innerHTML = `form {
position: relative;
@ -140,7 +140,6 @@ function init() {
subtree: true,
});
}, 300);
}
async function cmdTip() {
initDom();
@ -334,9 +333,10 @@ function initDom() {
delete window.__cmd_list;
delete window.__cmd_index;
}
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
init();
cmdInit();
} else {
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', cmdInit);
}

11
scripts/core.js vendored
View File

@ -4,7 +4,7 @@
* @url https://github.com/lencx/ChatGPT/tree/main/scripts/core.js
*/
var coreInit = (() => {
function coreInit() {
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
function transformCallback(callback = () => {}, once = false) {
const identifier = uid();
@ -21,6 +21,7 @@ var coreInit = (() => {
});
return identifier;
}
async function invoke(cmd, args) {
return new Promise((resolve, reject) => {
if (!window.__TAURI_POST_MESSAGE__) reject('__TAURI_POST_MESSAGE__ does not exist!');
@ -226,11 +227,11 @@ var coreInit = (() => {
return { startLoading, stopLoading };
}
return { init };
})();
init();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
coreInit.init();
coreInit();
} else {
document.addEventListener('DOMContentLoaded', coreInit.init);
document.addEventListener('DOMContentLoaded', coreInit);
}

6
scripts/dalle2.js vendored
View File

@ -4,7 +4,7 @@
* @url https://github.com/lencx/ChatGPT/tree/main/scripts/dalle2.js
*/
function init() {
function dalle2Init() {
document.addEventListener('click', (e) => {
const origin = e.target.closest('a');
if (!origin || !origin.target) return;
@ -35,7 +35,7 @@ function init() {
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
init();
dalle2Init();
} else {
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', dalle2Init);
}

10
scripts/export.js vendored
View File

@ -4,7 +4,7 @@
* @url https://github.com/lencx/ChatGPT/tree/main/scripts/export.js
*/
async function init() {
async function exportInit() {
if (window.location.pathname === '/auth/login') return;
const buttonOuterHTMLFallback = `<button class="btn flex justify-center gap-2 btn-neutral">Try Again</button>`;
removeButtons();
@ -303,7 +303,7 @@ async function init() {
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const formattedDateTime = `${year}_${month}_${day}_${hours}_${minutes}_${seconds}`;
const formattedDateTime = `${year}_${month}_${day}-${hours}${minutes}${seconds}`;
return formattedDateTime;
}
@ -315,10 +315,10 @@ async function init() {
}
}
window.addEventListener('resize', init);
window.addEventListener('resize', exportInit);
if (document.readyState === 'complete' || document.readyState === 'interactive') {
init();
exportInit();
} else {
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', exportInit);
}

5
scripts/main.js vendored Normal file
View File

@ -0,0 +1,5 @@
// *** ChatGPT User Script ***
// @github: https://github.com/lencx/ChatGPT
// @path: $HOME/.chatgpt/scripts/main.js
console.log('Hello, ChatGPT!');

View File

@ -12,6 +12,10 @@
"name": "cmd.js",
"version": "0.1.0"
},
{
"name": "chat.js",
"version": "0.1.0"
},
{
"name": "core.js",
"version": "0.1.0"

View File

@ -4,7 +4,7 @@
* @url https://github.com/lencx/ChatGPT/tree/main/scripts/popup.core.js
*/
async function init() {
async function popupCoreInit() {
const chatConf = (await invoke('get_app_conf')) || {};
if (!chatConf.popup_search) return;
if (!window.FloatingUIDOM) return;
@ -77,7 +77,7 @@ async function init() {
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
init();
popupCoreInit();
} else {
document.addEventListener('DOMContentLoaded', init);
document.addEventListener('DOMContentLoaded', popupCoreInit);
}

View File

@ -9,8 +9,6 @@ use std::{collections::HashMap, fs, path::PathBuf, vec};
use tauri::{api, command, AppHandle, Manager};
use walkdir::WalkDir;
use super::fs_extra::Error;
#[command]
pub fn get_chat_prompt_cmd() -> serde_json::Value {
let path = utils::app_root().join("chat.prompt.cmd.json");
@ -183,13 +181,11 @@ pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<PromptRecord>
},
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["chatgpt-prompts".to_string()],
tags: vec!["awesome-chatgpt-prompts".to_string()],
enable: true,
})
.collect::<Vec<PromptRecord>>();
let data2 = transformed_data;
let prompts = utils::app_root().join("chat.prompt.json");
let prompt_cmd = utils::app_root().join("chat.prompt.cmd.json");
let chatgpt_prompts = utils::app_root()
@ -211,7 +207,7 @@ pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<PromptRecord>
// chatgpt_prompts.json
fs::write(
chatgpt_prompts,
serde_json::to_string_pretty(&data).unwrap(),
serde_json::to_string_pretty(&transformed_data).unwrap(),
)
.unwrap();
let cmd_data = cmd_list();
@ -253,7 +249,7 @@ pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<PromptRecord>
window::cmd::window_reload(app.clone(), "core");
window::cmd::window_reload(app, "tray");
return Some(data2);
return Some(transformed_data);
}
}

View File

@ -93,9 +93,6 @@ pub fn init() -> Menu {
#[cfg(target_os = "macos")]
hide_dock_icon_menu.into(),
system_tray_menu.into(),
CustomMenuItem::new("inject_script", "Inject Script")
.accelerator("CmdOrCtrl+J")
.into(),
MenuItem::Separator.into(),
Submenu::new(
"Theme",
@ -219,7 +216,6 @@ pub fn init() -> Menu {
pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
let win = Some(event.window()).unwrap();
let app = win.app_handle();
let script_path = utils::script_path().to_string_lossy().to_string();
let menu_id = event.menu_item_id();
let menu_handle = win.menu_handle();
@ -239,7 +235,6 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
// Preferences
"control_center" => window::cmd::control_window(app, "control".into()),
"restart" => tauri::api::process::restart(&app.env()),
"inject_script" => open(&app, &script_path),
"go_conf" => utils::open_file(utils::app_root()),
"clear_conf" => utils::clear_conf(&app),
"app_website" => window::cmd::wa_window(

View File

@ -4,4 +4,5 @@ pub mod gpt;
pub mod menu;
pub mod script;
pub mod setup;
pub mod template;
pub mod window;

View File

@ -1,4 +1,4 @@
use crate::utils::{app_root, exists};
use crate::utils::{app_root, create_file, exists};
use log::error;
use log::info;
use regex::Regex;
@ -6,10 +6,13 @@ use serde_json::{from_str, json, Value};
use std::fs;
use tauri::Manager;
use crate::{conf::SCRIPTS_MANIFEST, window};
use crate::{conf::SCRIPTS_DIR, window};
pub async fn init_script(app: tauri::AppHandle) -> anyhow::Result<(), reqwest::Error> {
let body = reqwest::get(SCRIPTS_MANIFEST).await?.text().await?;
let body = reqwest::get(format!("{}{}", SCRIPTS_DIR, "manifest.json"))
.await?
.text()
.await?;
if exist_scripts("manifest.json".into()) {
let compare = compare_nested_json_objects(
@ -39,8 +42,16 @@ pub async fn init_script(app: tauri::AppHandle) -> anyhow::Result<(), reqwest::E
Ok(())
}
pub fn parse_script(name: String) -> serde_json::Value {
let code = &fs::read_to_string(name).unwrap();
pub fn parse_script(name: String) -> Option<serde_json::Value> {
let script_file = app_root().join("scripts").join(name);
let code = match fs::read_to_string(&script_file) {
Ok(content) => content,
Err(_) => {
error!("parse_script_error: {}", script_file.display());
return None;
}
};
// let code = &fs::read_to_string(name).unwrap();
let re_name = Regex::new(r"@name\s+(.*?)\n").unwrap();
let re_version = Regex::new(r"@version\s+(.*?)\n").unwrap();
let re_url = Regex::new(r"@url\s+(.*?)\n").unwrap();
@ -49,15 +60,15 @@ pub fn parse_script(name: String) -> serde_json::Value {
let mut version = String::new();
let mut url = String::new();
if let Some(capture) = re_name.captures(code) {
if let Some(capture) = re_name.captures(&code) {
name = capture.get(1).unwrap().as_str().trim().to_owned();
}
if let Some(capture) = re_version.captures(code) {
if let Some(capture) = re_version.captures(&code) {
version = capture.get(1).unwrap().as_str().trim().to_owned();
}
if let Some(capture) = re_url.captures(code) {
if let Some(capture) = re_url.captures(&code) {
url = capture.get(1).unwrap().as_str().trim().to_owned();
}
@ -67,7 +78,7 @@ pub fn parse_script(name: String) -> serde_json::Value {
"url": url,
});
json_data
Some(json_data)
}
pub fn exist_scripts(file: String) -> bool {
@ -77,9 +88,16 @@ pub fn exist_scripts(file: String) -> bool {
pub fn create_chatgpt_scripts(file: String, body: String) {
let script_file = app_root().join("scripts").join(file);
match create_file(&script_file) {
Ok(_) => {
info!("script_file: {:?}", script_file);
fs::write(&script_file, body).unwrap();
}
Err(e) => {
error!("create_file, {}: {}", script_file.display(), e);
}
}
}
fn compare_nested_json_objects(json1: &str, json2: &str) -> bool {
let value1: Value = from_str(json1).unwrap_or_else(|err| {
@ -121,3 +139,53 @@ pub fn compare_json_objects(obj1: &Value, obj2: &Value) -> bool {
_ => obj1 == obj2,
}
}
pub mod cmd {
use super::{create_chatgpt_scripts, parse_script};
use crate::conf::SCRIPTS_DIR;
use log::{error, info};
use tauri::Manager;
#[tauri::command]
pub fn get_script_info(name: String) -> Option<serde_json::Value> {
parse_script(name)
}
#[tauri::command]
pub async fn sync_scripts(app: tauri::AppHandle, name: String) -> bool {
let res = reqwest::get(format!("{}{}", SCRIPTS_DIR, name)).await;
info!("sync_scripts: {}", name);
let body = match res {
Ok(response) => match response.text().await {
Ok(text) => text,
Err(err) => {
error!("sync_scripts_result_error: {}", err);
if let Some(v) = app.get_window("core") {
tauri::api::dialog::message(
Some(&v),
"Sync Scripts Error",
format!("sync_scripts_result_error: {}", err),
);
}
return false;
}
},
Err(err) => {
error!("sync_scripts_response_error: {}", err);
if let Some(v) = app.get_window("core") {
tauri::api::dialog::message(
Some(&v),
"Sync Scripts Error",
format!("sync_scripts_response_error: {}", err),
);
}
return false;
}
};
create_chatgpt_scripts(name, body);
true
}
}

View File

@ -1,4 +1,8 @@
use crate::{app, conf::AppConf, utils};
use crate::{
app,
conf::AppConf,
utils::{self, load_script},
};
use log::{error, info};
use tauri::{utils::config::WindowUrl, window::WindowBuilder, App, GlobalShortcutManager, Manager};
use wry::application::accelerator::Accelerator;
@ -10,6 +14,8 @@ pub fn init(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
let url = app_conf.main_origin.to_string();
let theme = AppConf::theme_mode();
app::template::Template::new(utils::app_root().join("scripts"));
let handle = app.app_handle();
tauri::async_runtime::spawn(async move {
app::script::init_script(handle)
@ -71,7 +77,7 @@ pub fn init(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
.theme(Some(theme))
.always_on_top(app_conf2.stay_on_top)
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../../../scripts/core.js"))
.initialization_script(&load_script("core.js"))
.user_agent(&app_conf2.ua_window);
#[cfg(target_os = "macos")]
@ -89,11 +95,11 @@ pub fn init(app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
.initialization_script(include_str!("../vendors/jspdf.js"))
.initialization_script(include_str!("../vendors/turndown.js"))
.initialization_script(include_str!("../vendors/turndown-plugin-gfm.js"))
.initialization_script(include_str!("../../../scripts/popup.core.js"))
.initialization_script(include_str!("../../../scripts/export.js"))
.initialization_script(include_str!("../../../scripts/markdown.export.js"))
.initialization_script(include_str!("../../../scripts/cmd.js"))
.initialization_script(include_str!("../../../scripts/chat.js"))
.initialization_script(&load_script("popup.core.js"))
.initialization_script(&load_script("export.js"))
.initialization_script(&load_script("markdown.export.js"))
.initialization_script(&load_script("cmd.js"))
.initialization_script(&load_script("chat.js"))
}
main_win.build().unwrap();

View File

@ -0,0 +1,110 @@
use anyhow::Result;
use log::{error, info};
use std::{
fs::{self, File},
io::Write,
path::Path,
};
pub static SCRIPT_MAIN: &[u8] = include_bytes!("../../../scripts/main.js");
pub static SCRIPT_CORE: &[u8] = include_bytes!("../../../scripts/core.js");
pub static SCRIPT_CHAT: &[u8] = include_bytes!("../../../scripts/chat.js");
pub static SCRIPT_CMD: &[u8] = include_bytes!("../../../scripts/cmd.js");
pub static SCRIPT_DALLE2: &[u8] = include_bytes!("../../../scripts/dalle2.js");
pub static SCRIPT_EXPORT: &[u8] = include_bytes!("../../../scripts/export.js");
pub static SCRIPT_MD_EXPORT: &[u8] = include_bytes!("../../../scripts/markdown.export.js");
pub static SCRIPT_POPUP_CORE: &[u8] = include_bytes!("../../../scripts/popup.core.js");
pub static SCRIPT_MANIFEST: &[u8] = include_bytes!("../../../scripts/manifest.json");
pub static SCRIPT_README: &[u8] = include_bytes!("../../../scripts/README.md");
#[derive(Debug)]
pub struct Template {
pub main: Vec<u8>,
pub core: Vec<u8>,
pub chat: Vec<u8>,
pub cmd: Vec<u8>,
pub dalle2: Vec<u8>,
pub export: Vec<u8>,
pub markdown_export: Vec<u8>,
pub popup_core: Vec<u8>,
pub manifest: Vec<u8>,
pub readme: Vec<u8>,
}
impl Template {
pub fn new<P: AsRef<Path>>(template_dir: P) -> Self {
let template_dir = template_dir.as_ref();
let mut template = Template::default();
{
let files = vec![
(template_dir.join("main.js"), &mut template.main),
(template_dir.join("core.js"), &mut template.core),
(template_dir.join("chat.js"), &mut template.chat),
(template_dir.join("cmd.js"), &mut template.cmd),
(template_dir.join("dalle2.js"), &mut template.dalle2),
(template_dir.join("export.js"), &mut template.export),
(
template_dir.join("markdown.export.js"),
&mut template.markdown_export,
),
(template_dir.join("popup.core.js"), &mut template.popup_core),
(template_dir.join("README.md"), &mut template.readme),
(template_dir.join("manifest.json"), &mut template.manifest),
];
for (filename, dest) in files {
if !filename.exists() {
match create_dir(&filename) {
Ok(_) => {
if let Err(e) = write_file_contents(&filename, dest) {
error!("write_script, {}: {}", filename.display(), e);
} else {
info!("write_script: {}", filename.display());
}
}
Err(e) => {
error!("create_file, {}: {}", filename.display(), e);
}
}
}
}
}
template
}
}
impl Default for Template {
fn default() -> Template {
Template {
main: Vec::from(SCRIPT_MAIN),
core: Vec::from(SCRIPT_CORE),
chat: Vec::from(SCRIPT_CHAT),
cmd: Vec::from(SCRIPT_CMD),
dalle2: Vec::from(SCRIPT_DALLE2),
export: Vec::from(SCRIPT_EXPORT),
markdown_export: Vec::from(SCRIPT_MD_EXPORT),
popup_core: Vec::from(SCRIPT_POPUP_CORE),
manifest: Vec::from(SCRIPT_MANIFEST),
readme: Vec::from(SCRIPT_README),
}
}
}
fn create_dir<P: AsRef<Path>>(filename: P) -> Result<()> {
let filename = filename.as_ref();
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
Ok(())
}
pub fn write_file_contents<P: AsRef<Path>>(filename: P, data: &[u8]) -> Result<()> {
let filename = filename.as_ref();
let mut file = File::create(filename)?;
file.write_all(data)?;
Ok(())
}

View File

@ -1,4 +1,7 @@
use crate::{conf::AppConf, utils};
use crate::{
conf::AppConf,
utils::{self, load_script},
};
use log::info;
use std::time::SystemTime;
use tauri::{utils::config::WindowUrl, window::WindowBuilder, Manager};
@ -23,16 +26,16 @@ pub fn tray_window(handle: &tauri::AppHandle) {
.always_on_top(true)
.theme(Some(theme))
.initialization_script(&utils::user_script())
.initialization_script(include_str!("../../../scripts/core.js"))
.initialization_script(&load_script("core.js"))
.user_agent(&app_conf.ua_tray);
if app_conf.tray_origin == "https://chat.openai.com" && !app_conf.tray_dashboard {
tray_win = tray_win
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
.initialization_script(include_str!("../../../scripts/cmd.js"))
.initialization_script(include_str!("../../../scripts/chat.js"))
.initialization_script(include_str!("../../../scripts/popup.core.js"))
.initialization_script(&load_script("cmd.js"))
.initialization_script(&load_script("chat.js"))
.initialization_script(&load_script("popup.core.js"))
}
tray_win.build().unwrap().hide().unwrap();
@ -78,9 +81,9 @@ pub fn dalle2_window(
.inner_size(800.0, 600.0)
.always_on_top(false)
.theme(Some(theme))
.initialization_script(include_str!("../../../scripts/core.js"))
.initialization_script(&load_script("core.js"))
.initialization_script(&load_script("dalle2.js"))
.initialization_script(&query)
.initialization_script(include_str!("../../../scripts/dalle2.js"))
.build()
.unwrap();
});
@ -161,7 +164,7 @@ pub mod cmd {
tauri::async_runtime::spawn(async move {
tauri::WindowBuilder::new(&app, label, tauri::WindowUrl::App(url.parse().unwrap()))
.initialization_script(&script.unwrap_or_default())
.initialization_script(include_str!("../../../scripts/core.js"))
.initialization_script(&load_script("core.js"))
.title(title)
.inner_size(960.0, 700.0)
.resizable(true)

View File

@ -15,8 +15,7 @@ pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPD
// pub const BUY_COFFEE: &str = "https://www.buymeacoffee.com/lencx";
pub const GITHUB_PROMPTS_CSV_URL: &str =
"https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv";
pub const SCRIPTS_MANIFEST: &str =
"https://raw.githubusercontent.com/lencx/ChatGPT/main/scripts/manifest.json";
pub const SCRIPTS_DIR: &str = "https://raw.githubusercontent.com/lencx/ChatGPT/main/scripts/";
pub const APP_CONF_PATH: &str = "chat.conf.json";
pub const CHATGPT_URL: &str = "https://chat.openai.com";

View File

@ -7,7 +7,7 @@ mod app;
mod conf;
mod utils;
use app::{cmd, fs_extra, gpt, menu, setup, window};
use app::{cmd, fs_extra, gpt, menu, script, setup, window};
use conf::AppConf;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_log::{
@ -76,6 +76,8 @@ async fn main() {
conf::cmd::form_confirm,
conf::cmd::form_cancel,
conf::cmd::form_msg,
script::cmd::sync_scripts,
script::cmd::get_script_info,
window::cmd::wa_window,
window::cmd::control_window,
window::cmd::window_reload,

View File

@ -4,7 +4,7 @@ use regex::Regex;
use serde_json::Value;
use std::{
collections::HashMap,
fs::{self, File},
fs,
path::{Path, PathBuf},
process::Command,
};
@ -25,11 +25,15 @@ pub fn exists(path: &Path) -> bool {
Path::new(path).exists()
}
pub fn create_file(path: &Path) -> Result<File> {
if let Some(p) = path.parent() {
fs::create_dir_all(p)?
pub fn create_file<P: AsRef<Path>>(filename: P) -> Result<()> {
let filename = filename.as_ref();
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
File::create(path).map_err(Into::into)
}
fs::File::create(filename)?;
Ok(())
}
pub fn create_chatgpt_prompts() {
@ -42,31 +46,20 @@ pub fn create_chatgpt_prompts() {
}
}
pub fn script_path() -> PathBuf {
let script_file = app_root().join("main.js");
if !exists(&script_file) {
create_file(&script_file).unwrap();
fs::write(
&script_file,
format!(
"// *** ChatGPT User Script ***\n// @github: https://github.com/lencx/ChatGPT \n// @path: {}\n\nconsole.log('🤩 Hello ChatGPT!!!');",
&script_file.to_string_lossy()
),
)
.unwrap();
}
script_file
}
pub fn user_script() -> String {
let user_script_content = fs::read_to_string(script_path()).unwrap_or_else(|_| "".to_string());
let user_script_file = app_root().join("scripts").join("main.js");
let user_script_content = fs::read_to_string(user_script_file).unwrap_or_else(|_| "".to_string());
format!(
"window.addEventListener('DOMContentLoaded', function() {{\n{}\n}})",
user_script_content
)
}
pub fn load_script(filename: &str) -> String {
let script_file = app_root().join("scripts").join(filename);
fs::read_to_string(script_file).unwrap_or_else(|_| "".to_string())
}
pub fn open_file(path: PathBuf) {
let pathname = convert_path(path.to_str().unwrap());
info!("open_file: {}", pathname);

View File

@ -16,7 +16,8 @@ const FilePath: FC<FilePathProps> = ({ className, label = 'PATH', paths = '', ur
const [filePath, setPath] = useState('');
useEffect(() => {
if (!path && !url) return;
if (!(paths || url)) return;
(async () => {
if (url) {
setPath(url);

View File

@ -74,6 +74,7 @@ export const EditRow: FC<EditRowProps> = ({ rowKey, row, actions }) => {
onChange={handleChange}
{...DISABLE_AUTO_COMPLETE}
onPressEnter={handleSave}
onBlur={handleSave}
/>
) : (
<div className="rowedit" onClick={handleEdit}>

22
src/main.scss vendored
View File

@ -114,3 +114,25 @@ body,
cursor: pointer;
}
}
.editor-task {
margin-bottom: 5px;
display: flex;
justify-content: space-between;
.ant-breadcrumb-link {
padding: 3px 5px;
transition: all 300ms ease;
border-radius: 4px;
&:hover {
color: rgba(0, 0, 0, 0.88);
background-color: rgba(0, 0, 0, 0.06);
cursor: pointer;
}
}
.editor-btn {
cursor: pointer;
margin-left: 5px;
}
}

6
src/routes.tsx vendored
View File

@ -14,6 +14,7 @@ import type { MenuProps } from 'antd';
import Settings from '@/view/settings';
import About from '@/view/about';
import Scripts from '@/view/scripts';
import ScriptsEditor from '@/view/scripts/Editor';
import UserCustom from '@/view/prompts/UserCustom';
import SyncPrompts from '@/view/prompts/SyncPrompts';
import SyncCustom from '@/view/prompts/SyncCustom';
@ -103,6 +104,11 @@ export const routes: Array<ChatRouteObject> = [
icon: <CodeOutlined />,
},
},
{
path: '/scripts/:id',
element: <ScriptsEditor />,
hideMenu: true,
},
{
path: '/about',
element: <About />,

4
src/utils.ts vendored
View File

@ -23,6 +23,10 @@ export const chatRoot = async () => {
return join(await homeDir(), '.chatgpt');
};
export const scriptRoot = async () => {
return join(await chatRoot(), 'scripts');
};
export const chatPromptPath = async (): Promise<string> => {
return join(await chatRoot(), CHAT_PROMPT_JSON);
};

View File

@ -1,16 +0,0 @@
.md-task {
margin-bottom: 5px;
display: flex;
justify-content: space-between;
.ant-breadcrumb-link {
padding: 3px 5px;
transition: all 300ms ease;
border-radius: 4px;
&:hover {
color: rgba(0, 0, 0, 0.88);
background-color: rgba(0, 0, 0, 0.06);
cursor: pointer;
}
}
}

View File

@ -8,7 +8,6 @@ import { fs, shell } from '@tauri-apps/api';
import useInit from '@/hooks/useInit';
import SplitIcon from '@/icons/SplitIcon';
import { getPath } from '@/view/notes/config';
import './index.scss';
const modeMap: any = {
0: 'split',
@ -41,7 +40,7 @@ export default function Markdown() {
return (
<>
<div className="md-task">
<div className="editor-task">
<Breadcrumb separator="">
<Breadcrumb.Item onClick={() => history.go(-1)}>
<ArrowLeftOutlined />

View File

@ -67,5 +67,5 @@ const RenderPath = ({ row }: any) => {
};
export const getPath = async (row: any) => {
return (await path.join(await chatRoot(), 'notes', row.id)) + `.${row.ext}`;
return await path.join(await chatRoot(), 'notes', `${row.id}.md`);
};

View File

@ -7,7 +7,6 @@ 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>
@ -17,20 +16,17 @@ export const syncColumns = () => [
title: 'Act',
dataIndex: 'act',
key: 'act',
// width: 200,
},
{
title: 'Tags',
dataIndex: 'tags',
key: 'tags',
// width: 150,
render: () => <Tag>chatgpt-prompts</Tag>,
render: () => <Tag>built-in</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')} />
),
@ -40,7 +36,6 @@ export const syncColumns = () => [
title: 'Prompt',
dataIndex: 'prompt',
key: 'prompt',
// width: 300,
render: (v: string) => <span className="chat-prompts-val">{v}</span>,
},
];

View File

@ -12,7 +12,8 @@ import { fmtDate, chatRoot } from '@/utils';
import { syncColumns } from './config';
import './index.scss';
const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv';
// const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv';
const promptsURL = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv';
export default function SyncPrompts() {
const { rowSelection, selectedRowIDs } = useTableRowSelection();
@ -83,7 +84,7 @@ export default function SyncPrompts() {
</div>
<div className="chat-table-tip">
<div className="chat-sync-path">
<FilePath url={promptsURL} content="f/awesome-chatgpt-prompts/prompts.csv" />
<FilePath label="URL" url={promptsURL} content="f/awesome-chatgpt-prompts/prompts.csv" />
<FilePath label="CACHE" paths="cache_prompts/chatgpt_prompts.json" />
</div>
{lastUpdated && (

View File

@ -7,7 +7,6 @@ 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>
@ -17,13 +16,11 @@ export const syncColumns = () => [
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) => (
@ -36,7 +33,6 @@ export const syncColumns = () => [
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')} />
),
@ -46,7 +42,6 @@ export const syncColumns = () => [
title: 'Prompt',
dataIndex: 'prompt',
key: 'prompt',
// width: 300,
render: (v: string) => <span className="chat-prompts-val">{v}</span>,
},
];

View File

@ -73,7 +73,7 @@ export default function SyncRecord() {
</div>
<div className="chat-table-tip">
<div className="chat-sync-path">
<FilePath url={filePath} />
<FilePath label="URL" url={filePath} />
<FilePath label="CACHE" paths={`cache_prompts/${state?.id}.json`} />
</div>
{state?.last_updated && (

View File

@ -45,12 +45,6 @@ const UserCustomForm: ForwardRefRenderFunction<FormProps, UserCustomFormProps> =
>
<Input placeholder="Please enter the 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"
@ -58,6 +52,12 @@ const UserCustomForm: ForwardRefRenderFunction<FormProps, UserCustomFormProps> =
>
<Input.TextArea rows={4} placeholder="Please enter a prompt" {...DISABLE_AUTO_COMPLETE} />
</Form.Item>
<Form.Item label="Enable" name="enable" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item label="Tags" name="tags">
<Tags value={record?.tags} />
</Form.Item>
</Form>
);
};

View File

@ -1,32 +1,74 @@
import { FC, useEffect, useState } from 'react';
import { FC, useState } from 'react';
import { useLocation } from 'react-router-dom';
import Editor from '@monaco-editor/react';
import { Breadcrumb, Tag, Popconfirm } from 'antd';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { fs, shell, dialog, process } from '@tauri-apps/api';
interface MarkdownEditorProps {
value?: string;
import useInit from '@/hooks/useInit';
import { getPath } from './config';
interface ScriptEditorProps {
onChange?: (v: string) => void;
mode?: string;
}
const ScriptEditor: FC<MarkdownEditorProps> = ({
value = 'console.log',
onChange,
mode = 'split',
}) => {
const [content, setContent] = useState(value);
const ScriptEditor: FC<ScriptEditorProps> = ({ onChange }) => {
const [filePath, setFilePath] = useState('');
const [source, setSource] = useState('// write your script here\n');
const location = useLocation();
const state = location?.state;
useEffect(() => {
setContent(value);
onChange && onChange(value);
}, [value]);
useInit(async () => {
const file = await getPath(state);
setFilePath(file);
setSource(await fs.readTextFile(file));
});
const handleEdit = (e: any) => {
setContent(e);
setSource(e);
onChange && onChange(e);
};
const handleSave = async () => {
await fs.writeTextFile(filePath, source);
const isOk = await dialog.ask(
'The script will take effect after the application is restarted. Do you want to restart now?',
{
title: 'Script saved successfully',
},
);
if (isOk) {
process.relaunch();
}
};
const handleReset = async () => {
setSource(await fs.readTextFile(filePath));
};
return (
<div className="script-editor">
<Editor language="js" value={content} onChange={handleEdit} />
<Breadcrumb className="editor-task" separator="">
<Breadcrumb.Item onClick={() => history.go(-1)}>
<ArrowLeftOutlined />
</Breadcrumb.Item>
<Popconfirm
placement="topRight"
title="Are you sure you want to save the changes? It is a risky operation, but you can restore it through the sync button."
onConfirm={handleSave}
onCancel={handleReset}
okText="Yes"
cancelText="No"
overlayStyle={{ width: 300 }}
>
<Tag className="editor-btn" color="#108ee9">
Save
</Tag>
</Popconfirm>
<Breadcrumb.Item onClick={() => shell.open(filePath)}>{filePath}</Breadcrumb.Item>
</Breadcrumb>
<Editor height="80vh" language="javascript" value={source} onChange={handleEdit} />
</div>
);
};

View File

@ -1,97 +0,0 @@
import { useEffect, useState } from 'react';
import { Tag, Collapse, Tooltip } from 'antd';
import { EditOutlined, FileSyncOutlined } from '@ant-design/icons';
import { path, fs, shell } from '@tauri-apps/api';
import { chatRoot } from '@/utils';
import useInit from '@/hooks/useInit';
export type ScriptInfo = {
name: string;
filePath: string;
file: string;
};
interface ScriptHeadProps {
name: string;
onEdit?: (data: ScriptInfo) => void;
activeKey: string;
}
export default function ScriptHead({ name, onEdit, activeKey }: ScriptHeadProps) {
const [file, setFile] = useState('');
const [filePath, setFilePath] = useState('');
const [editing, setEdit] = useState(false);
useEffect(() => {
if (activeKey !== name) {
setEdit(false);
}
}, [activeKey]);
useInit(async () => {
const filePath = await path.join(await chatRoot(), 'scripts', name);
setFilePath(filePath);
const content = await fs.readTextFile(filePath);
setFile(content);
});
const handleGoFile = () => {
shell.open(filePath);
};
const handleEdit = async () => {
setEdit(true);
onEdit && onEdit({ name, filePath, file });
};
const handleCancel = async () => {
setEdit(false);
};
const handleSave = async () => {
setEdit(false);
};
const handleSync = async () => {};
const handleURL = async () => {
shell.open(`https://github.com/lencx/ChatGPT/blob/main/scripts/${name}`);
};
const version = '0.1.0';
return (
<>
<span>
<Tag color="orange">{version}</Tag>
</span>
{editing ? (
<span>
<Tag className="action-btn" onClick={handleCancel} color="default">
Cancel
</Tag>
<Tag className="action-btn" onClick={handleSave} color="geekblue-inverse">
Save
</Tag>
</span>
) : (
<Tag className="action-btn" title="Script Edit" onClick={handleEdit} color="blue-inverse">
<EditOutlined />
</Tag>
)}
<Tag className="action-btn" title="Script Sync" onClick={handleSync} color="orange-inverse">
<FileSyncOutlined />
</Tag>
<span>
<Tag className="file-path" color="blue" onClick={handleGoFile}>
Path: {filePath}
</Tag>
</span>
<span>
<Tag className="file-path" color="green" onClick={handleURL}>
URL: lencx/ChatGPT/scripts/{name}
</Tag>
</span>
</>
);
}

View File

@ -1,25 +1,37 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { Tag, Space, Popconfirm } from 'antd';
import { ArrowRightOutlined } from '@ant-design/icons';
import { path, shell } from '@tauri-apps/api';
import useInit from '@/hooks/useInit';
import { fmtDate, chatRoot } from '@/utils';
export const scriptColumns = () => [
export const scriptColumns = ({ scriptsMap }: any) => [
{
title: 'File Name',
dataIndex: 'name',
key: 'name',
width: 120,
render: (v: string) => <Tag>{v}</Tag>,
fixed: 'left',
width: 160,
render: (v: string) => <Tag color={v === 'main.js' ? 'green' : 'default'}>{v}</Tag>,
},
{
title: 'Version',
dataIndex: 'version',
key: 'version',
width: 120,
render: (v: string) => <Tag>{v}</Tag>,
width: 200,
render: (_: string, row: any) => {
const next = scriptsMap?.[row.name]?.next_version;
const curr = scriptsMap?.[row.name]?.curr_version;
return (
row.name !== 'main.js' && (
<Space>
{curr && <Tag style={{ marginRight: 0 }}>{curr}</Tag>}
{next && next !== curr && <ArrowRightOutlined style={{ color: '#989898' }} />}
{next && next !== curr && <Tag color="green-inverse">{next}</Tag>}
</Space>
)
);
},
},
{
title: 'Path',
@ -28,6 +40,14 @@ export const scriptColumns = () => [
width: 200,
render: (_: string, row: any) => <RenderPath row={row} />,
},
{
title: 'Remote File',
width: 200,
render: (_: string, row: any) => {
const uri = `https://raw.githubusercontent.com/lencx/ChatGPT/main/scripts/${row.name}`;
return <a onClick={() => shell.open(uri)}>{uri}</a>;
},
},
{
title: 'Created',
dataIndex: 'created',
@ -38,28 +58,41 @@ export const scriptColumns = () => [
{
title: 'Action',
fixed: 'right',
width: 160,
width: 100,
render: (_: any, row: any, actions: any) => {
const isExternal = row.name === 'main.js';
const next = scriptsMap?.[row.name]?.next_version;
const curr = scriptsMap?.[row.name]?.curr_version;
return (
<Space>
<Link to={`/md/${row.id}`} state={row}>
<Link
to={`/scripts/${row.id}`}
state={row}
style={{ color: !isExternal ? '#ff4d4f' : '' }}
>
Edit
</Link>
{!isExternal && next && next !== curr && (
<Popconfirm
placement="topLeft"
title="Are you sure you want to synchronize? It will overwrite all previous modifications made to this file."
onConfirm={() => actions.setRecord(row, 'sync')}
okText="Yes"
cancelText="No"
overlayStyle={{ width: 300 }}
>
<a>Sync</a>
</Popconfirm>
)}
</Space>
);
},
},
];
const RenderPath = ({ row }: any) => {
export const RenderPath = ({ row }: any) => {
const [filePath, setFilePath] = useState('');
useInit(async () => {
setFilePath(await getPath(row));
@ -68,5 +101,5 @@ const RenderPath = ({ row }: any) => {
};
export const getPath = async (row: any) => {
return (await path.join(await chatRoot(), 'notes', row.id)) + `.${row.ext}`;
return await path.join(await chatRoot(), 'scripts', `${row.name}`);
};

View File

@ -1,10 +0,0 @@
.chatgpt-script {
.ant-collapse-header {
user-select: none;
-webkit-user-select: none;
}
}
.script-editor {
height: 300px;
}

View File

@ -1,17 +1,15 @@
import { useState } from 'react';
import { Table, Tag } from 'antd';
import { useEffect, useState } from 'react';
import { Table } from 'antd';
import { path, fs, invoke } from '@tauri-apps/api';
import useData from '@/hooks/useData';
import useInit from '@/hooks/useInit';
import useColumns from '@/hooks/useColumns';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { TABLE_PAGINATION } from '@/hooks/useTable';
import { scriptRoot } from '@/utils';
import { scriptColumns } from './config';
import ScriptHead, { type ScriptInfo } from './Head';
import ScriptEditor from './Editor';
import './index.scss';
// const { Panel } = Collapse;
const SCRIPTS = [
'main.js',
'core.js',
'chat.js',
'cmd.js',
@ -22,51 +20,54 @@ const SCRIPTS = [
];
export default function Scripts() {
const [activeKey, setActiveKey] = useState('core.js');
const [scriptsMap, setScriptsMap] = useState({});
const { columns, ...opInfo } = useColumns(scriptColumns({ scriptsMap }));
const { columns, ...opInfo } = useColumns(scriptColumns());
const handleInit = async () => {
try {
const manifestPath = await path.join(await scriptRoot(), 'manifest.json');
const data = await fs.readTextFile(manifestPath);
const { scripts } = JSON.parse(data);
const infoMap: Record<string, any> = {};
const handleActiveKeyChange = (key: any) => {
setActiveKey(key);
for (const script of scripts) {
const scriptInfo: any = await invoke('get_script_info', { name: script.name });
infoMap[script.name] = {
curr_version: scriptInfo?.version,
next_version: script.version,
};
}
setScriptsMap(infoMap);
} catch (error) {
console.error(error);
}
};
const panelHeadProps = {
onEdit(data: ScriptInfo) {
setActiveKey(data.name);
},
activeKey,
};
useInit(handleInit);
useEffect(() => {
if (!opInfo.opType) return;
(async () => {
if (opInfo.opType === 'sync') {
const isOk = await invoke('sync_scripts', { name: opInfo?.opRecord?.name });
if (isOk) {
await handleInit();
opInfo.resetRecord();
}
}
})();
}, [opInfo.opType]);
return (
<div className="chatgpt-script">
<Table
rowKey="name"
scroll={{ x: 800 }}
columns={columns}
dataSource={SCRIPTS.map((i) => ({ name: i }))}
{...TABLE_PAGINATION}
pagination={false}
/>
{/* <Tabs
items={SCRIPTS.map((i) => {
return {
label: <Tag>{i}</Tag>,
key: i,
children: <ScriptEditor />,
}
})}
/> */}
{/* <Collapse
accordion
collapsible="icon"
activeKey={activeKey}
onChange={handleActiveKeyChange}
>
{SCRIPTS.map((i) => {
return (
<Panel header={<ScriptHead name={i} {...panelHeadProps} />} key={i}>
<ScriptEditor />
</Panel>
)
})}
</Collapse> */}
</div>
);
}