mirror of
https://github.com/lencx/ChatGPT.git
synced 2024-10-01 01:06:13 -04:00
Merge pull request #206 from lencx/dev
This commit is contained in:
commit
26bd845a72
@ -201,8 +201,9 @@ Mac 上无法安装,提示开发者未验证,具体可以查看下面给出
|
||||
|
||||
#### 预安装
|
||||
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
- [VS Code](https://code.visualstudio.com/)
|
||||
- [Rust (必须)](https://www.rust-lang.org/)
|
||||
- [Node.js (必须)](https://nodejs.org/)
|
||||
- [VS Code (可选)](https://code.visualstudio.com/)
|
||||
- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
- [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode)
|
||||
|
||||
@ -226,6 +227,9 @@ yarn dev
|
||||
yarn build
|
||||
```
|
||||
|
||||
- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180)
|
||||
- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182)
|
||||
|
||||
## ❤️ 感谢
|
||||
|
||||
- 分享按钮的代码从 [@liady](https://github.com/liady) 的插件获得,并做了一些本地化修改
|
||||
|
10
README.md
10
README.md
@ -88,7 +88,7 @@ You can look at **[awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt
|
||||
## ✨ Features
|
||||
|
||||
- Multi-platform: `macOS` `Linux` `Windows`
|
||||
- Export ChatGPT history (PNG, PDF and Share Link)
|
||||
- Export ChatGPT history (PNG, PDF and Markdown)
|
||||
- Automatic application upgrade notification
|
||||
- Common shortcut keys
|
||||
- System tray hover window
|
||||
@ -209,8 +209,9 @@ It's safe, just a wrapper for [OpenAI ChatGPT](https://chat.openai.com) website,
|
||||
|
||||
#### PreInstall
|
||||
|
||||
- [Rust](https://www.rust-lang.org/)
|
||||
- [VS Code](https://code.visualstudio.com/)
|
||||
- [Rust (Required)](https://www.rust-lang.org/)
|
||||
- [Node.js (Required)](https://nodejs.org/)
|
||||
- [VS Code (Optional)](https://code.visualstudio.com/)
|
||||
- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
- [tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode)
|
||||
|
||||
@ -234,6 +235,9 @@ yarn dev
|
||||
yarn build
|
||||
```
|
||||
|
||||
- [The distDir configuration is set to "../dist" but this path doesn't exist](https://github.com/lencx/ChatGPT/discussions/180)
|
||||
- [Error A public key has been found, but no private key. Make sure to set TAURI_PRIVATE_KEY environment variable.](https://github.com/lencx/ChatGPT/discussions/182)
|
||||
|
||||
## ❤️ Thanks
|
||||
|
||||
- The core implementation of the share button code was copied from the [@liady](https://github.com/liady) extension with some modifications.
|
||||
|
@ -1,5 +1,14 @@
|
||||
# UPDATE LOG
|
||||
|
||||
## v0.9.0
|
||||
|
||||
fix:
|
||||
- export button does not work
|
||||
|
||||
feat:
|
||||
- add an export markdown button
|
||||
- `Control Center` adds `Notes` and `Download` menus for managing exported chat files (Markdown, PNG, PDF). `Notes` supports markdown previews.
|
||||
|
||||
## v0.8.1
|
||||
|
||||
fix:
|
||||
|
@ -40,7 +40,9 @@
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.4",
|
||||
"react-router-dom": "^6.4.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -50,6 +52,7 @@
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"sass": "^1.56.2",
|
||||
|
@ -16,18 +16,18 @@ tauri-build = {version = "1.2.1", features = [] }
|
||||
[dependencies]
|
||||
anyhow = "1.0.66"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.2.3", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] }
|
||||
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
|
||||
log = "0.4.17"
|
||||
csv = "1.1.6"
|
||||
thiserror = "1.0.38"
|
||||
walkdir = "2.3.2"
|
||||
regex = "1.7.0"
|
||||
tokio = { version = "1.23.0", features = ["macros"] }
|
||||
reqwest = "0.11.13"
|
||||
wry = "0.23.4"
|
||||
wry = "0.24.1"
|
||||
dark-light = "1.0.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.23.0", features = ["macros"] }
|
||||
tauri-plugin-positioner = { version = "1.0.4", features = ["system-tray"] }
|
||||
tauri = { version = "1.2.3", features = ["api-all", "devtools", "global-shortcut", "system-tray", "updater"] }
|
||||
[dependencies.tauri-plugin-log]
|
||||
git = "https://github.com/lencx/tauri-plugin-log"
|
||||
branch = "dev"
|
||||
@ -36,6 +36,8 @@ features = ["colored"]
|
||||
git = "https://github.com/lencx/tauri-plugin-autostart"
|
||||
branch = "dev"
|
||||
|
||||
# sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite"] }
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
|
@ -1,10 +1,11 @@
|
||||
use crate::{
|
||||
app::window,
|
||||
app::{fs_extra, window},
|
||||
conf::{ChatConfJson, GITHUB_PROMPTS_CSV_URL},
|
||||
utils,
|
||||
utils::{self, chat_root, create_file},
|
||||
};
|
||||
use log::info;
|
||||
use std::{collections::HashMap, fs, path::PathBuf};
|
||||
use regex::Regex;
|
||||
use std::{collections::HashMap, fs, path::PathBuf, vec};
|
||||
use tauri::{api, command, AppHandle, Manager, Theme};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
@ -35,11 +36,20 @@ pub fn fullscreen(app: AppHandle) {
|
||||
|
||||
#[command]
|
||||
pub fn download(_app: AppHandle, name: String, blob: Vec<u8>) {
|
||||
let path = api::path::download_dir().unwrap().join(name);
|
||||
let path = chat_root().join(PathBuf::from(name));
|
||||
create_file(&path).unwrap();
|
||||
fs::write(&path, blob).unwrap();
|
||||
utils::open_file(path);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn save_file(_app: AppHandle, name: String, content: String) {
|
||||
let path = chat_root().join(PathBuf::from(name));
|
||||
create_file(&path).unwrap();
|
||||
fs::write(&path, content).unwrap();
|
||||
utils::open_file(path);
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn open_link(app: AppHandle, url: String) {
|
||||
api::shell::open(&app.shell_scope(), url, None).unwrap();
|
||||
@ -167,6 +177,93 @@ pub fn cmd_list() -> Vec<ModelRecord> {
|
||||
list
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
|
||||
pub struct FileMetadata {
|
||||
pub name: String,
|
||||
pub ext: String,
|
||||
pub created: u64,
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_download_list(pathname: &str) -> (Vec<serde_json::Value>, PathBuf) {
|
||||
info!("get_download_list: {}", pathname);
|
||||
let download_path = chat_root().join(PathBuf::from(pathname));
|
||||
let content = fs::read_to_string(&download_path).unwrap_or_else(|err| {
|
||||
info!("download_list_error: {}", err);
|
||||
fs::write(&download_path, "[]").unwrap();
|
||||
"[]".to_string()
|
||||
});
|
||||
let list = serde_json::from_str::<Vec<serde_json::Value>>(&content).unwrap_or_else(|err| {
|
||||
info!("download_list_parse_error: {}", err);
|
||||
vec![]
|
||||
});
|
||||
|
||||
(list, download_path)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub fn download_list(pathname: &str, dir: &str, filename: Option<String>, id: Option<String>) {
|
||||
info!("download_list: {}", pathname);
|
||||
let data = get_download_list(pathname);
|
||||
let mut list = vec![];
|
||||
let mut idmap = HashMap::new();
|
||||
utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap);
|
||||
|
||||
for entry in WalkDir::new(utils::chat_root().join(dir))
|
||||
.into_iter()
|
||||
.filter_entry(|e| !utils::is_hidden(e))
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
let metadata = entry.metadata().unwrap();
|
||||
if metadata.is_file() {
|
||||
let file_path = entry.path().display().to_string();
|
||||
let re = Regex::new(r"(?P<id>[\d\w]+).(?P<ext>\w+)$").unwrap();
|
||||
let caps = re.captures(&file_path).unwrap();
|
||||
let fid = &caps["id"];
|
||||
let fext = &caps["ext"];
|
||||
|
||||
let mut file_data = FileMetadata {
|
||||
name: fid.to_string(),
|
||||
id: fid.to_string(),
|
||||
ext: fext.to_string(),
|
||||
created: fs_extra::system_time_to_ms(metadata.created()),
|
||||
};
|
||||
|
||||
if idmap.get(fid).is_some() {
|
||||
let name = idmap.get(fid).unwrap().get("name").unwrap().clone();
|
||||
match name {
|
||||
serde_json::Value::String(v) => {
|
||||
file_data.name = v.clone();
|
||||
v
|
||||
}
|
||||
_ => "".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if filename.is_some() && id.is_some() {
|
||||
if let Some(ref v) = id {
|
||||
if fid == v {
|
||||
if let Some(ref v2) = filename {
|
||||
file_data.name = v2.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
list.push(serde_json::to_value(file_data).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
// dbg!(&list);
|
||||
list.sort_by(|a, b| {
|
||||
let a1 = a.get("created").unwrap().as_u64().unwrap();
|
||||
let b1 = b.get("created").unwrap().as_u64().unwrap();
|
||||
a1.cmp(&b1).reverse()
|
||||
});
|
||||
|
||||
fs::write(data.1, serde_json::to_string_pretty(&list).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<ModelRecord>> {
|
||||
let res = utils::get_data(GITHUB_PROMPTS_CSV_URL, Some(&app))
|
||||
|
@ -60,7 +60,7 @@ struct UnixMetadata {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Metadata {
|
||||
accessed_at_ms: u64,
|
||||
created_at_ms: u64,
|
||||
pub created_at_ms: u64,
|
||||
modified_at_ms: u64,
|
||||
is_dir: bool,
|
||||
is_file: bool,
|
||||
@ -74,7 +74,7 @@ pub struct Metadata {
|
||||
file_attributes: u32,
|
||||
}
|
||||
|
||||
fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
|
||||
pub fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
|
||||
time.map(|t| {
|
||||
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
|
||||
duration_since_epoch.as_millis() as u64
|
||||
|
@ -41,10 +41,6 @@ pub fn init() -> Menu {
|
||||
stay_on_top
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let titlebar =
|
||||
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
|
||||
|
||||
let theme_light = CustomMenuItem::new("theme_light".to_string(), "Light");
|
||||
let theme_dark = CustomMenuItem::new("theme_dark".to_string(), "Dark");
|
||||
let theme_system = CustomMenuItem::new("theme_system".to_string(), "System");
|
||||
@ -62,6 +58,9 @@ pub fn init() -> Menu {
|
||||
popup_search
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let titlebar =
|
||||
CustomMenuItem::new("titlebar".to_string(), "Titlebar").accelerator("CmdOrCtrl+B");
|
||||
#[cfg(target_os = "macos")]
|
||||
let titlebar_menu = if chat_conf.titlebar {
|
||||
titlebar.selected()
|
||||
@ -69,6 +68,13 @@ pub fn init() -> Menu {
|
||||
titlebar
|
||||
};
|
||||
|
||||
let system_tray = CustomMenuItem::new("system_tray".to_string(), "System Tray");
|
||||
let system_tray_menu = if chat_conf.tray {
|
||||
system_tray.selected()
|
||||
} else {
|
||||
system_tray
|
||||
};
|
||||
|
||||
let preferences_menu = Submenu::new(
|
||||
"Preferences",
|
||||
Menu::with_items([
|
||||
@ -81,6 +87,7 @@ pub fn init() -> Menu {
|
||||
titlebar_menu.into(),
|
||||
#[cfg(target_os = "macos")]
|
||||
CustomMenuItem::new("hide_dock_icon".to_string(), "Hide Dock Icon").into(),
|
||||
system_tray_menu.into(),
|
||||
CustomMenuItem::new("inject_script".to_string(), "Inject Script")
|
||||
.accelerator("CmdOrCtrl+J")
|
||||
.into(),
|
||||
@ -141,6 +148,7 @@ pub fn init() -> Menu {
|
||||
CustomMenuItem::new("awesome".to_string(), "Awesome ChatGPT")
|
||||
.accelerator("CmdOrCtrl+Shift+A")
|
||||
.into(),
|
||||
CustomMenuItem::new("buy_coffee".to_string(), "Buy lencx a coffee").into(),
|
||||
]),
|
||||
);
|
||||
|
||||
@ -242,6 +250,7 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
|
||||
"go_conf" => utils::open_file(utils::chat_root()),
|
||||
"clear_conf" => utils::clear_conf(&app),
|
||||
"awesome" => open(&app, conf::AWESOME_URL.to_string()),
|
||||
"buy_coffee" => open(&app, conf::BUY_COFFEE.to_string()),
|
||||
"popup_search" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
let popup_search = !chat_conf.popup_search;
|
||||
@ -281,6 +290,11 @@ pub fn menu_handler(event: WindowMenuEvent<tauri::Wry>) {
|
||||
.unwrap();
|
||||
tauri::api::process::restart(&app.env());
|
||||
}
|
||||
"system_tray" => {
|
||||
let chat_conf = conf::ChatConfJson::get_chat_conf();
|
||||
ChatConfJson::amend(&serde_json::json!({ "tray": !chat_conf.tray }), None).unwrap();
|
||||
tauri::api::process::restart(&app.env());
|
||||
}
|
||||
"theme_light" | "theme_dark" | "theme_system" => {
|
||||
let theme = match menu_id {
|
||||
"theme_dark" => "Dark",
|
||||
|
@ -61,14 +61,18 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
|
||||
.always_on_top(chat_conf.stay_on_top)
|
||||
.title_bar_style(ChatConfJson::titlebar())
|
||||
.initialization_script(&utils::user_script())
|
||||
.initialization_script(include_str!("../vendors/jq.js"))
|
||||
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
|
||||
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
|
||||
.initialization_script(include_str!("../vendors/html2canvas.js"))
|
||||
.initialization_script(include_str!("../vendors/jspdf.js"))
|
||||
.initialization_script(include_str!("../assets/core.js"))
|
||||
.initialization_script(include_str!("../assets/popup.core.js"))
|
||||
.initialization_script(include_str!("../assets/export.js"))
|
||||
.initialization_script(include_str!("../assets/cmd.js"))
|
||||
.initialization_script(include_str!("../vendors/turndown.js"))
|
||||
.initialization_script(include_str!("../vendors/turndown-plugin-gfm.js"))
|
||||
.initialization_script(include_str!("../scripts/core.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"))
|
||||
.user_agent(&chat_conf.ua_window)
|
||||
.build()
|
||||
.unwrap();
|
||||
@ -82,14 +86,18 @@ pub fn init(app: &mut App) -> std::result::Result<(), Box<dyn std::error::Error>
|
||||
.theme(theme)
|
||||
.always_on_top(chat_conf.stay_on_top)
|
||||
.initialization_script(&utils::user_script())
|
||||
.initialization_script(include_str!("../vendors/jq.js"))
|
||||
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
|
||||
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
|
||||
.initialization_script(include_str!("../vendors/html2canvas.js"))
|
||||
.initialization_script(include_str!("../vendors/jspdf.js"))
|
||||
.initialization_script(include_str!("../assets/core.js"))
|
||||
.initialization_script(include_str!("../assets/popup.core.js"))
|
||||
.initialization_script(include_str!("../assets/export.js"))
|
||||
.initialization_script(include_str!("../assets/cmd.js"))
|
||||
.initialization_script(include_str!("../vendors/turndown.js"))
|
||||
.initialization_script(include_str!("../vendors/turndown-plugin-gfm.js"))
|
||||
.initialization_script(include_str!("../scripts/core.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"))
|
||||
.user_agent(&chat_conf.ua_window)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
@ -18,11 +18,12 @@ pub fn tray_window(handle: &tauri::AppHandle) {
|
||||
.always_on_top(true)
|
||||
.theme(theme)
|
||||
.initialization_script(&utils::user_script())
|
||||
.initialization_script(include_str!("../vendors/jq.js"))
|
||||
.initialization_script(include_str!("../vendors/floating-ui-core.js"))
|
||||
.initialization_script(include_str!("../vendors/floating-ui-dom.js"))
|
||||
.initialization_script(include_str!("../assets/core.js"))
|
||||
.initialization_script(include_str!("../assets/cmd.js"))
|
||||
.initialization_script(include_str!("../assets/popup.core.js"))
|
||||
.initialization_script(include_str!("../scripts/core.js"))
|
||||
.initialization_script(include_str!("../scripts/cmd.js"))
|
||||
.initialization_script(include_str!("../scripts/popup.core.js"))
|
||||
.user_agent(&chat_conf.ua_tray)
|
||||
.build()
|
||||
.unwrap()
|
||||
@ -73,9 +74,10 @@ pub fn dalle2_window(
|
||||
.inner_size(800.0, 600.0)
|
||||
.always_on_top(false)
|
||||
.theme(theme)
|
||||
.initialization_script(include_str!("../assets/core.js"))
|
||||
.initialization_script(include_str!("../vendors/jq.js"))
|
||||
.initialization_script(include_str!("../scripts/core.js"))
|
||||
.initialization_script(&query)
|
||||
.initialization_script(include_str!("../assets/dalle2.js"))
|
||||
.initialization_script(include_str!("../scripts/dalle2.js"))
|
||||
.build()
|
||||
.unwrap();
|
||||
});
|
||||
|
@ -14,12 +14,14 @@ use tauri::TitleBarStyle;
|
||||
pub const ISSUES_URL: &str = "https://github.com/lencx/ChatGPT/issues";
|
||||
pub const UPDATE_LOG_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/UPDATE_LOG.md";
|
||||
pub const AWESOME_URL: &str = "https://github.com/lencx/ChatGPT/blob/main/AWESOME.md";
|
||||
pub const 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 DEFAULT_CHAT_CONF: &str = r#"{
|
||||
"stay_on_top": false,
|
||||
"auto_update": "Prompt",
|
||||
"theme": "Light",
|
||||
"tray": true,
|
||||
"titlebar": true,
|
||||
"popup_search": true,
|
||||
"global_shortcut": "",
|
||||
@ -33,6 +35,7 @@ pub const DEFAULT_CHAT_CONF_MAC: &str = r#"{
|
||||
"stay_on_top": false,
|
||||
"auto_update": "Prompt",
|
||||
"theme": "Light",
|
||||
"tray": true,
|
||||
"titlebar": false,
|
||||
"popup_search": true,
|
||||
"global_shortcut": "",
|
||||
@ -53,6 +56,7 @@ pub struct ChatConfJson {
|
||||
pub theme: String,
|
||||
// auto update policy, Prompt/Silent/Disable
|
||||
pub auto_update: String,
|
||||
pub tray: bool,
|
||||
pub popup_search: bool,
|
||||
pub stay_on_top: bool,
|
||||
pub default_origin: String,
|
||||
|
@ -30,7 +30,12 @@ async fn main() {
|
||||
trace: Color::Cyan,
|
||||
};
|
||||
|
||||
tauri::Builder::default()
|
||||
cmd::download_list("chat.download.json", "download", None, None);
|
||||
cmd::download_list("chat.notes.json", "notes", None, None);
|
||||
|
||||
let chat_conf = ChatConfJson::get_chat_conf();
|
||||
|
||||
let mut builder = tauri::Builder::default()
|
||||
// https://github.com/tauri-apps/tauri/pull/2736
|
||||
.plugin(
|
||||
LoggerBuilder::new()
|
||||
@ -45,10 +50,16 @@ async fn main() {
|
||||
])
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_positioner::init())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
cmd::drag_window,
|
||||
cmd::fullscreen,
|
||||
cmd::download,
|
||||
cmd::save_file,
|
||||
cmd::open_link,
|
||||
cmd::get_chat_conf,
|
||||
cmd::get_theme,
|
||||
@ -65,16 +76,18 @@ async fn main() {
|
||||
cmd::window_reload,
|
||||
cmd::dalle2_window,
|
||||
cmd::cmd_list,
|
||||
cmd::download_list,
|
||||
cmd::get_download_list,
|
||||
fs_extra::metadata,
|
||||
])
|
||||
.setup(setup::init)
|
||||
.plugin(tauri_plugin_positioner::init())
|
||||
.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
None,
|
||||
))
|
||||
.menu(menu::init())
|
||||
.system_tray(menu::tray_menu())
|
||||
.menu(menu::init());
|
||||
|
||||
if chat_conf.tray {
|
||||
builder = builder.system_tray(menu::tray_menu());
|
||||
}
|
||||
|
||||
builder
|
||||
.on_menu_event(menu::menu_handler)
|
||||
.on_system_tray_event(menu::tray_handler)
|
||||
.on_window_event(|event| {
|
||||
|
@ -1,6 +1,6 @@
|
||||
// *** Core Script - CMD ***
|
||||
|
||||
function init() {
|
||||
$(function() {
|
||||
const styleDom = document.createElement('style');
|
||||
styleDom.innerHTML = `form {
|
||||
position: relative;
|
||||
@ -71,9 +71,9 @@ function init() {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.chatappico.pdf {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
.chatappico.pdf, .chatappico.md {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
#download-png-button, #download-pdf-button, #download-html-button {
|
||||
@ -92,7 +92,7 @@ function init() {
|
||||
clearInterval(window.formInterval);
|
||||
cmdTip();
|
||||
}, 200);
|
||||
}
|
||||
});
|
||||
|
||||
async function cmdTip() {
|
||||
const chatModelJson = await invoke('get_chat_model_cmd') || {};
|
||||
@ -269,12 +269,3 @@ async function cmdTip() {
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
@ -40,7 +40,7 @@ window.uid = uid;
|
||||
window.invoke = invoke;
|
||||
window.transformCallback = transformCallback;
|
||||
|
||||
async function init() {
|
||||
$(async function () {
|
||||
if (__TAURI_METADATA__.__currentWindow.label === 'tray') {
|
||||
document.getElementsByTagName('html')[0].style['font-size'] = '70%';
|
||||
}
|
||||
@ -91,13 +91,4 @@ async function init() {
|
||||
window.__sync_prompts = async function() {
|
||||
await invoke('sync_prompts', { time: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
// *** Core Script - DALL·E 2 ***
|
||||
|
||||
async function init() {
|
||||
$(function () {
|
||||
document.addEventListener("click", (e) => {
|
||||
const origin = e.target.closest("a");
|
||||
if (!origin || !origin.target) return;
|
||||
@ -28,13 +28,4 @@ async function init() {
|
||||
searchInput.value = query;
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
})
|
@ -1,8 +1,8 @@
|
||||
// *** Core Script - Export ***
|
||||
// @ref: https://github.com/liady/ChatGPT-pdf
|
||||
|
||||
const buttonOuterHTMLFallback = `<button class="btn flex justify-center gap-2 btn-neutral" id="download-png-button">Try Again</button>`;
|
||||
async function init() {
|
||||
|
||||
$(async function () {
|
||||
if (window.innerWidth < 767) return;
|
||||
const chatConf = await invoke('get_chat_conf') || {};
|
||||
if (window.buttonsInterval) {
|
||||
@ -25,7 +25,7 @@ async function init() {
|
||||
removeButtons();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
|
||||
const Format = {
|
||||
PNG: "png",
|
||||
@ -49,14 +49,19 @@ function shouldAddButtons(actionsArea) {
|
||||
const buttons = actionsArea.querySelectorAll("button");
|
||||
|
||||
const hasTryAgainButton = Array.from(buttons).some((button) => {
|
||||
return !button.id?.includes("download");
|
||||
return !/download-/.test(button.id);
|
||||
});
|
||||
|
||||
// fix: https://github.com/lencx/ChatGPT/issues/189
|
||||
if (buttons.length === 1) {
|
||||
const stopBtn = buttons?.[0]?.innerText;
|
||||
|
||||
if (/Stop generating/ig.test(stopBtn)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buttons.length === 2 && (/Regenerate response/ig.test(stopBtn) || buttons[1].innerText === '')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasTryAgainButton && buttons.length === 1) {
|
||||
return true;
|
||||
}
|
||||
@ -80,51 +85,58 @@ function shouldAddButtons(actionsArea) {
|
||||
function removeButtons() {
|
||||
const downloadButton = document.getElementById("download-png-button");
|
||||
const downloadPdfButton = document.getElementById("download-pdf-button");
|
||||
const downloadHtmlButton = document.getElementById("download-html-button");
|
||||
const downloadMdButton = document.getElementById("download-markdown-button");
|
||||
if (downloadButton) {
|
||||
downloadButton.remove();
|
||||
}
|
||||
if (downloadPdfButton) {
|
||||
downloadPdfButton.remove();
|
||||
}
|
||||
if (downloadHtmlButton) {
|
||||
downloadHtmlButton.remove();
|
||||
if (downloadPdfButton) {
|
||||
downloadMdButton.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function addActionsButtons(actionsArea, TryAgainButton) {
|
||||
const downloadButton = TryAgainButton.cloneNode(true);
|
||||
// Export markdown
|
||||
const exportMd = TryAgainButton.cloneNode(true);
|
||||
exportMd.id = "download-markdown-button";
|
||||
downloadButton.setAttribute("share-ext", "true");
|
||||
exportMd.title = "Export Markdown";
|
||||
exportMd.innerHTML = setIcon('md');
|
||||
exportMd.onclick = () => {
|
||||
exportMarkdown();
|
||||
};
|
||||
actionsArea.appendChild(exportMd);
|
||||
|
||||
// Generate PNG
|
||||
downloadButton.id = "download-png-button";
|
||||
downloadButton.setAttribute("share-ext", "true");
|
||||
// downloadButton.innerText = "Generate PNG";
|
||||
downloadButton.title = "Generate PNG";
|
||||
downloadButton.innerHTML = setIcon('png');
|
||||
downloadButton.onclick = () => {
|
||||
downloadThread();
|
||||
};
|
||||
actionsArea.appendChild(downloadButton);
|
||||
|
||||
// Generate PDF
|
||||
const downloadPdfButton = TryAgainButton.cloneNode(true);
|
||||
downloadPdfButton.id = "download-pdf-button";
|
||||
downloadButton.setAttribute("share-ext", "true");
|
||||
// downloadPdfButton.innerText = "Download PDF";
|
||||
downloadPdfButton.title = "Download PDF";
|
||||
downloadPdfButton.innerHTML = setIcon('pdf');
|
||||
downloadPdfButton.onclick = () => {
|
||||
downloadThread({ as: Format.PDF });
|
||||
};
|
||||
actionsArea.appendChild(downloadPdfButton);
|
||||
}
|
||||
|
||||
// fix: https://github.com/lencx/ChatGPT/issues/126
|
||||
// const exportHtml = TryAgainButton.cloneNode(true);
|
||||
// exportHtml.id = "download-html-button";
|
||||
// downloadButton.setAttribute("share-ext", "true");
|
||||
// // exportHtml.innerText = "Share Link";
|
||||
// exportHtml.title = "Share Link";
|
||||
// exportHtml.innerHTML = setIcon('link');
|
||||
// exportHtml.onclick = () => {
|
||||
// sendRequest();
|
||||
// };
|
||||
// actionsArea.appendChild(exportHtml);
|
||||
async function exportMarkdown() {
|
||||
const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML);
|
||||
const { id, filename } = getName();
|
||||
await invoke('save_file', { name: `notes/${id}.md`, content: data });
|
||||
await invoke('download_list', { pathname: 'chat.notes.json', filename, id, dir: 'notes' });
|
||||
}
|
||||
|
||||
function downloadThread({ as = Format.PNG } = {}) {
|
||||
@ -150,16 +162,18 @@ function downloadThread({ as = Format.PNG } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleImg(imgData) {
|
||||
async function handleImg(imgData) {
|
||||
const binaryData = atob(imgData.split("base64,")[1]);
|
||||
const data = [];
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
data.push(binaryData.charCodeAt(i));
|
||||
}
|
||||
invoke('download', { name: `chatgpt-${Date.now()}.png`, blob: Array.from(new Uint8Array(data)) });
|
||||
const { pathname, id, filename } = getName();
|
||||
await invoke('download', { name: `download/img/${id}.png`, blob: data });
|
||||
await invoke('download_list', { pathname, filename, id, dir: 'download' });
|
||||
}
|
||||
|
||||
function handlePdf(imgData, canvas, pixelRatio) {
|
||||
async function handlePdf(imgData, canvas, pixelRatio) {
|
||||
const { jsPDF } = window.jspdf;
|
||||
const orientation = canvas.width > canvas.height ? "l" : "p";
|
||||
var pdf = new jsPDF(orientation, "pt", [
|
||||
@ -169,9 +183,16 @@ function handlePdf(imgData, canvas, pixelRatio) {
|
||||
var pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
var pdfHeight = pdf.internal.pageSize.getHeight();
|
||||
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST');
|
||||
|
||||
const { pathname, id, filename } = getName();
|
||||
const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument());
|
||||
invoke('download', { name: `chatgpt-${Date.now()}.pdf`, blob: Array.from(new Uint8Array(data)) });
|
||||
await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) });
|
||||
await invoke('download_list', { pathname, filename, id, dir: 'download' });
|
||||
}
|
||||
|
||||
function getName() {
|
||||
const id = uid().toString(36);
|
||||
const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || '';
|
||||
return { filename: name ? name : id, id, pathname: 'chat.download.json' };
|
||||
}
|
||||
|
||||
class Elements {
|
||||
@ -187,9 +208,7 @@ class Elements {
|
||||
|
||||
// fix: old chat https://github.com/lencx/ChatGPT/issues/185
|
||||
if (!this.thread) {
|
||||
this.thread = document.querySelector(
|
||||
"main .overflow-y-auto"
|
||||
);
|
||||
this.thread = document.querySelector("main .overflow-y-auto");
|
||||
}
|
||||
|
||||
// h-full overflow-y-auto
|
||||
@ -245,67 +264,11 @@ class Elements {
|
||||
}
|
||||
}
|
||||
|
||||
function selectElementByClassPrefix(classPrefix) {
|
||||
const element = document.querySelector(`[class^='${classPrefix}']`);
|
||||
return element;
|
||||
}
|
||||
|
||||
async function sendRequest() {
|
||||
const data = getData();
|
||||
const uploadUrlResponse = await fetch(
|
||||
"https://chatgpt-static.s3.amazonaws.com/url.txt"
|
||||
);
|
||||
const uploadUrl = await uploadUrlResponse.text();
|
||||
fetch(uploadUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
invoke('open_link', { url: data.url });
|
||||
});
|
||||
}
|
||||
|
||||
function getData() {
|
||||
const globalCss = getCssFromSheet(
|
||||
document.querySelector("link[rel=stylesheet]").sheet
|
||||
);
|
||||
const localCss =
|
||||
getCssFromSheet(
|
||||
document.querySelector(`style[data-styled][data-styled-version]`).sheet
|
||||
) || "body{}";
|
||||
const data = {
|
||||
main: document.querySelector("main").outerHTML,
|
||||
// css: `${globalCss} /* GLOBAL-LOCAL */ ${localCss}`,
|
||||
globalCss,
|
||||
localCss,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
function getCssFromSheet(sheet) {
|
||||
return Array.from(sheet.cssRules)
|
||||
.map((rule) => rule.cssText)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// run init
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
|
||||
function setIcon(type) {
|
||||
return {
|
||||
link: `<svg class="chatappico" viewBox="0 0 1024 1024"><path d="M1007.382 379.672L655.374 75.702C624.562 49.092 576 70.694 576 112.03v160.106C254.742 275.814 0 340.2 0 644.652c0 122.882 79.162 244.618 166.666 308.264 27.306 19.862 66.222-5.066 56.154-37.262C132.132 625.628 265.834 548.632 576 544.17V720c0 41.4 48.6 62.906 79.374 36.328l352.008-304c22.142-19.124 22.172-53.506 0-72.656z" p-id="8506" fill="currentColor"></path></svg>`,
|
||||
// link: `<svg class="chatappico" viewBox="0 0 1024 1024"><path d="M1007.382 379.672L655.374 75.702C624.562 49.092 576 70.694 576 112.03v160.106C254.742 275.814 0 340.2 0 644.652c0 122.882 79.162 244.618 166.666 308.264 27.306 19.862 66.222-5.066 56.154-37.262C132.132 625.628 265.834 548.632 576 544.17V720c0 41.4 48.6 62.906 79.374 36.328l352.008-304c22.142-19.124 22.172-53.506 0-72.656z" p-id="8506" fill="currentColor"></path></svg>`,
|
||||
png: `<svg class="chatappico" viewBox="0 0 1070 1024"><path d="M981.783273 0H85.224727C38.353455 0 0 35.374545 0 83.083636v844.893091c0 47.616 38.353455 86.574545 85.178182 86.574546h903.633454c46.917818 0 81.733818-38.958545 81.733819-86.574546V83.083636C1070.592 35.374545 1028.701091 0 981.783273 0zM335.825455 135.912727c74.193455 0 134.330182 60.974545 134.330181 136.285091 0 75.170909-60.136727 136.192-134.330181 136.192-74.286545 0-134.516364-61.021091-134.516364-136.192 0-75.264 60.229818-136.285091 134.516364-136.285091z m-161.512728 745.937455a41.890909 41.890909 0 0 1-27.648-10.379637 43.752727 43.752727 0 0 1-4.654545-61.067636l198.097454-255.162182a42.123636 42.123636 0 0 1 57.716364-6.702545l116.549818 128.139636 286.906182-352.814545c14.615273-18.711273 90.251636-106.775273 135.866182-6.935273 0.093091-0.093091 0.093091 112.965818 0.232727 247.761455 0.093091 140.8 0.093091 317.067636 0.093091 317.067636-1.024-0.093091-762.740364 0.093091-763.112727 0.093091z" fill="currentColor"></path></svg>`,
|
||||
pdf: `<svg class="chatappico pdf" viewBox="0 0 1024 1024"><path d="M821.457602 118.382249H205.725895c-48.378584 0-87.959995 39.583368-87.959996 87.963909v615.731707c0 48.378584 39.581411 87.959995 87.959996 87.959996h615.733664c48.380541 0 87.961952-39.581411 87.961952-87.959996V206.346158c-0.001957-48.378584-39.583368-87.963909-87.963909-87.963909zM493.962468 457.544987c-10.112054 32.545237-21.72487 82.872662-38.806571 124.248336-8.806957 22.378397-8.380404 18.480717-15.001764 32.609808l5.71738-1.851007c58.760658-16.443827 99.901532-20.519564 138.162194-27.561607-7.67796-6.06371-14.350194-10.751884-19.631237-15.586807-26.287817-29.101504-35.464584-34.570387-70.440002-111.862636v0.003913z m288.36767 186.413594c-7.476424 8.356924-20.670227 13.191847-40.019704 13.191847-33.427694 0-63.808858-9.229597-107.79277-31.660824-75.648648 8.356924-156.097 17.214754-201.399704 31.729308-2.199293 0.876587-4.832967 1.759043-7.916674 3.077836-54.536215 93.237125-95.031389 132.767663-130.621199 131.19646-11.286054-0.49895-27.694661-7.044-32.973748-10.11988l-6.52157-6.196764-2.29517-4.353583c-3.07588-7.91863-3.954423-15.395054-2.197337-23.751977 4.838837-23.309771 29.907651-60.251638 82.686779-93.237126 8.356924-6.159587 27.430511-15.897917 45.020944-24.25484 13.311204-21.177004 19.45905-34.744531 36.341171-72.259702 19.102937-45.324228 36.505531-99.492589 47.500041-138.191543v-0.44025c-16.267727-53.219378-25.945401-89.310095-9.67376-147.80856 3.958337-16.71189 18.46702-33.864031 34.748444-33.864031h10.552304c10.115967 0 19.791684 3.520043 26.829814 10.552304 29.029107 29.031064 15.39114 103.824649 0.8805 162.323113-0.8805 2.63563-1.322707 4.832967-1.761 6.153717 17.59239 49.697378 45.400538 98.774492 73.108895 121.647926 11.436717 8.791304 22.638634 18.899444 36.71098 26.814161 19.791684-2.20125 37.517128-4.11487 55.547812-4.11487 54.540128 0 87.525615 9.67963 100.279169 30.351814 4.400543 7.034217 6.595923 15.389184 5.281043 24.1844-0.44025 10.996467-4.39663 21.112434-12.31526 29.031064z m-27.796407-36.748157c-4.394673-4.398587-17.024957-16.936907-78.601259-16.936907-3.073923 0-10.622744-0.784623-14.57521 3.612007 32.104987 14.072347 62.830525 24.757704 83.058545 24.757703 3.083707 0 5.72325-0.442207 8.356923-0.876586h1.759044c2.20125-0.8805 3.520043-1.324663 3.960293-5.71738-0.87463-1.324663-1.757087-3.083707-3.958336-4.838837z m-387.124553 63.041845c-9.237424 5.27713-16.71189 10.112054-21.112433 13.634053-31.226444 28.586901-51.018128 57.616008-53.217422 74.331812 19.789727-6.59788 45.737084-35.626987 74.329855-87.961952v-0.003913z m125.574957-297.822284l2.197336-1.761c3.079793-14.072347 5.232127-29.189554 7.87167-38.869184l1.318794-7.036174c4.39663-25.070771 2.71781-39.720334-4.76057-50.272637l-6.59788-2.20125a57.381208 57.381208 0 0 0-3.079794 5.27713c-7.474467 18.47289-7.063567 55.283661 3.0524 94.865072l-0.001956-0.001957z" fill="currentColor"></path></svg>`
|
||||
pdf: `<svg class="chatappico pdf" viewBox="0 0 1024 1024"><path d="M821.457602 118.382249H205.725895c-48.378584 0-87.959995 39.583368-87.959996 87.963909v615.731707c0 48.378584 39.581411 87.959995 87.959996 87.959996h615.733664c48.380541 0 87.961952-39.581411 87.961952-87.959996V206.346158c-0.001957-48.378584-39.583368-87.963909-87.963909-87.963909zM493.962468 457.544987c-10.112054 32.545237-21.72487 82.872662-38.806571 124.248336-8.806957 22.378397-8.380404 18.480717-15.001764 32.609808l5.71738-1.851007c58.760658-16.443827 99.901532-20.519564 138.162194-27.561607-7.67796-6.06371-14.350194-10.751884-19.631237-15.586807-26.287817-29.101504-35.464584-34.570387-70.440002-111.862636v0.003913z m288.36767 186.413594c-7.476424 8.356924-20.670227 13.191847-40.019704 13.191847-33.427694 0-63.808858-9.229597-107.79277-31.660824-75.648648 8.356924-156.097 17.214754-201.399704 31.729308-2.199293 0.876587-4.832967 1.759043-7.916674 3.077836-54.536215 93.237125-95.031389 132.767663-130.621199 131.19646-11.286054-0.49895-27.694661-7.044-32.973748-10.11988l-6.52157-6.196764-2.29517-4.353583c-3.07588-7.91863-3.954423-15.395054-2.197337-23.751977 4.838837-23.309771 29.907651-60.251638 82.686779-93.237126 8.356924-6.159587 27.430511-15.897917 45.020944-24.25484 13.311204-21.177004 19.45905-34.744531 36.341171-72.259702 19.102937-45.324228 36.505531-99.492589 47.500041-138.191543v-0.44025c-16.267727-53.219378-25.945401-89.310095-9.67376-147.80856 3.958337-16.71189 18.46702-33.864031 34.748444-33.864031h10.552304c10.115967 0 19.791684 3.520043 26.829814 10.552304 29.029107 29.031064 15.39114 103.824649 0.8805 162.323113-0.8805 2.63563-1.322707 4.832967-1.761 6.153717 17.59239 49.697378 45.400538 98.774492 73.108895 121.647926 11.436717 8.791304 22.638634 18.899444 36.71098 26.814161 19.791684-2.20125 37.517128-4.11487 55.547812-4.11487 54.540128 0 87.525615 9.67963 100.279169 30.351814 4.400543 7.034217 6.595923 15.389184 5.281043 24.1844-0.44025 10.996467-4.39663 21.112434-12.31526 29.031064z m-27.796407-36.748157c-4.394673-4.398587-17.024957-16.936907-78.601259-16.936907-3.073923 0-10.622744-0.784623-14.57521 3.612007 32.104987 14.072347 62.830525 24.757704 83.058545 24.757703 3.083707 0 5.72325-0.442207 8.356923-0.876586h1.759044c2.20125-0.8805 3.520043-1.324663 3.960293-5.71738-0.87463-1.324663-1.757087-3.083707-3.958336-4.838837z m-387.124553 63.041845c-9.237424 5.27713-16.71189 10.112054-21.112433 13.634053-31.226444 28.586901-51.018128 57.616008-53.217422 74.331812 19.789727-6.59788 45.737084-35.626987 74.329855-87.961952v-0.003913z m125.574957-297.822284l2.197336-1.761c3.079793-14.072347 5.232127-29.189554 7.87167-38.869184l1.318794-7.036174c4.39663-25.070771 2.71781-39.720334-4.76057-50.272637l-6.59788-2.20125a57.381208 57.381208 0 0 0-3.079794 5.27713c-7.474467 18.47289-7.063567 55.283661 3.0524 94.865072l-0.001956-0.001957z" fill="currentColor"></path></svg>`,
|
||||
md: `<svg class="chatappico md" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1380" width="200" height="200"><path d="M128 128h768a42.666667 42.666667 0 0 1 42.666667 42.666667v682.666666a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V170.666667a42.666667 42.666667 0 0 1 42.666667-42.666667z m170.666667 533.333333v-170.666666l85.333333 85.333333 85.333333-85.333333v170.666666h85.333334v-298.666666h-85.333334l-85.333333 85.333333-85.333333-85.333333H213.333333v298.666666h85.333334z m469.333333-128v-170.666666h-85.333333v170.666666h-85.333334l128 128 128-128h-85.333333z" p-id="1381" fill="currentColor"></path></svg>`
|
||||
}[type];
|
||||
}
|
36
src-tauri/src/scripts/markdown.export.js
vendored
Normal file
36
src-tauri/src/scripts/markdown.export.js
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
var ExportMD = (function () {
|
||||
if (!TurndownService || !turndownPluginGfm) return;
|
||||
const hljsREG = /^.*(hljs).*(language-[a-z0-9]+).*$/i;
|
||||
const gfm = turndownPluginGfm.gfm
|
||||
const turndownService = new TurndownService()
|
||||
.use(gfm)
|
||||
.addRule('code', {
|
||||
filter: (node) => {
|
||||
if (node.nodeName === 'CODE' && hljsREG.test(node.classList.value)) {
|
||||
return 'code';
|
||||
}
|
||||
},
|
||||
replacement: (content, node) => {
|
||||
const classStr = node.getAttribute('class');
|
||||
if (hljsREG.test(classStr)) {
|
||||
const lang = classStr.match(/.*language-(\w+)/)[1];
|
||||
if (lang) {
|
||||
return `\`\`\`${lang}\n${content}\n\`\`\``;
|
||||
}
|
||||
return `\`\`\`\n${content}\n\`\`\``;
|
||||
}
|
||||
}
|
||||
})
|
||||
.addRule('ignore', {
|
||||
filter: ['button', 'img'],
|
||||
replacement: () => '',
|
||||
})
|
||||
.addRule('table', {
|
||||
filter: 'table',
|
||||
replacement: function(content, node) {
|
||||
return `\`\`\`${content}\n\`\`\``;
|
||||
},
|
||||
});
|
||||
|
||||
return turndownService;
|
||||
}({}));
|
@ -1,6 +1,6 @@
|
||||
// *** Core Script - DALL·E 2 Core ***
|
||||
|
||||
async function init() {
|
||||
$(async function () {
|
||||
const chatConf = await invoke('get_chat_conf') || {};
|
||||
if (!chatConf.popup_search) return;
|
||||
if (!window.FloatingUIDOM) return;
|
||||
@ -71,14 +71,4 @@ async function init() {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (
|
||||
document.readyState === "complete" ||
|
||||
document.readyState === "interactive"
|
||||
) {
|
||||
init();
|
||||
} else {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
}
|
||||
})
|
@ -230,3 +230,23 @@ pub async fn silent_install(app: AppHandle<Wry>, update: UpdateResponse<Wry>) ->
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_hidden(entry: &walkdir::DirEntry) -> bool {
|
||||
entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.starts_with('.'))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn vec_to_hashmap(
|
||||
vec: impl Iterator<Item = serde_json::Value>,
|
||||
key: &str,
|
||||
map: &mut HashMap<String, serde_json::Value>,
|
||||
) {
|
||||
for v in vec {
|
||||
if let Some(kval) = v.get(key).and_then(serde_json::Value::as_str) {
|
||||
map.insert(kval.to_string(), v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
2
src-tauri/src/vendors/jq.js
vendored
Normal file
2
src-tauri/src/vendors/jq.js
vendored
Normal file
File diff suppressed because one or more lines are too long
164
src-tauri/src/vendors/turndown-plugin-gfm.js
vendored
Normal file
164
src-tauri/src/vendors/turndown-plugin-gfm.js
vendored
Normal file
@ -0,0 +1,164 @@
|
||||
var turndownPluginGfm = (function (exports) {
|
||||
'use strict';
|
||||
|
||||
var highlightRegExp = /highlight-(?:text|source)-([a-z0-9]+)/;
|
||||
|
||||
function highlightedCodeBlock (turndownService) {
|
||||
turndownService.addRule('highlightedCodeBlock', {
|
||||
filter: function (node) {
|
||||
var firstChild = node.firstChild;
|
||||
return (
|
||||
node.nodeName === 'DIV' &&
|
||||
highlightRegExp.test(node.className) &&
|
||||
firstChild &&
|
||||
firstChild.nodeName === 'PRE'
|
||||
)
|
||||
},
|
||||
replacement: function (content, node, options) {
|
||||
var className = node.className || '';
|
||||
var language = (className.match(highlightRegExp) || [null, ''])[1];
|
||||
|
||||
return (
|
||||
'\n\n' + options.fence + language + '\n' +
|
||||
node.firstChild.textContent +
|
||||
'\n' + options.fence + '\n\n'
|
||||
)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function strikethrough (turndownService) {
|
||||
turndownService.addRule('strikethrough', {
|
||||
filter: ['del', 's', 'strike'],
|
||||
replacement: function (content) {
|
||||
return '~' + content + '~'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var indexOf = Array.prototype.indexOf;
|
||||
var every = Array.prototype.every;
|
||||
var rules = {};
|
||||
|
||||
rules.tableCell = {
|
||||
filter: ['th', 'td'],
|
||||
replacement: function (content, node) {
|
||||
return cell(content, node)
|
||||
}
|
||||
};
|
||||
|
||||
rules.tableRow = {
|
||||
filter: 'tr',
|
||||
replacement: function (content, node) {
|
||||
var borderCells = '';
|
||||
var alignMap = { left: ':--', right: '--:', center: ':-:' };
|
||||
|
||||
if (isHeadingRow(node)) {
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
var border = '---';
|
||||
var align = (
|
||||
node.childNodes[i].getAttribute('align') || ''
|
||||
).toLowerCase();
|
||||
|
||||
if (align) border = alignMap[align] || border;
|
||||
|
||||
borderCells += cell(border, node.childNodes[i]);
|
||||
}
|
||||
}
|
||||
return '\n' + content + (borderCells ? '\n' + borderCells : '')
|
||||
}
|
||||
};
|
||||
|
||||
rules.table = {
|
||||
// Only convert tables with a heading row.
|
||||
// Tables with no heading row are kept using `keep` (see below).
|
||||
filter: function (node) {
|
||||
return node.nodeName === 'TABLE' && isHeadingRow(node.rows[0])
|
||||
},
|
||||
|
||||
replacement: function (content) {
|
||||
// Ensure there are no blank lines
|
||||
content = content.replace('\n\n', '\n');
|
||||
return '\n\n' + content + '\n\n'
|
||||
}
|
||||
};
|
||||
|
||||
rules.tableSection = {
|
||||
filter: ['thead', 'tbody', 'tfoot'],
|
||||
replacement: function (content) {
|
||||
return content
|
||||
}
|
||||
};
|
||||
|
||||
// A tr is a heading row if:
|
||||
// - the parent is a THEAD
|
||||
// - or if its the first child of the TABLE or the first TBODY (possibly
|
||||
// following a blank THEAD)
|
||||
// - and every cell is a TH
|
||||
function isHeadingRow (tr) {
|
||||
var parentNode = tr.parentNode;
|
||||
return (
|
||||
parentNode.nodeName === 'THEAD' ||
|
||||
(
|
||||
parentNode.firstChild === tr &&
|
||||
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
|
||||
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function isFirstTbody (element) {
|
||||
var previousSibling = element.previousSibling;
|
||||
return (
|
||||
element.nodeName === 'TBODY' && (
|
||||
!previousSibling ||
|
||||
(
|
||||
previousSibling.nodeName === 'THEAD' &&
|
||||
/^\s*$/i.test(previousSibling.textContent)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function cell (content, node) {
|
||||
var index = indexOf.call(node.parentNode.childNodes, node);
|
||||
var prefix = ' ';
|
||||
if (index === 0) prefix = '| ';
|
||||
return prefix + content + ' |'
|
||||
}
|
||||
|
||||
function tables (turndownService) {
|
||||
turndownService.keep(function (node) {
|
||||
return node.nodeName === 'TABLE' && !isHeadingRow(node.rows[0])
|
||||
});
|
||||
for (var key in rules) turndownService.addRule(key, rules[key]);
|
||||
}
|
||||
|
||||
function taskListItems (turndownService) {
|
||||
turndownService.addRule('taskListItems', {
|
||||
filter: function (node) {
|
||||
return node.type === 'checkbox' && node.parentNode.nodeName === 'LI'
|
||||
},
|
||||
replacement: function (content, node) {
|
||||
return (node.checked ? '[x]' : '[ ]') + ' '
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function gfm (turndownService) {
|
||||
turndownService.use([
|
||||
highlightedCodeBlock,
|
||||
strikethrough,
|
||||
tables,
|
||||
taskListItems
|
||||
]);
|
||||
}
|
||||
|
||||
exports.gfm = gfm;
|
||||
exports.highlightedCodeBlock = highlightedCodeBlock;
|
||||
exports.strikethrough = strikethrough;
|
||||
exports.tables = tables;
|
||||
exports.taskListItems = taskListItems;
|
||||
|
||||
return exports;
|
||||
}({}));
|
1
src-tauri/src/vendors/turndown.js
vendored
Normal file
1
src-tauri/src/vendors/turndown.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -7,7 +7,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "ChatGPT",
|
||||
"version": "0.8.1"
|
||||
"version": "0.9.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { FC, useState, useCallback } from 'react';
|
||||
import { Input } from 'antd';
|
||||
|
||||
import { DISABLE_AUTO_COMPLETE } from '@/utils';
|
||||
|
||||
export default function useColumns(columns: any[] = []) {
|
||||
const [opType, setOpType] = useState('');
|
||||
@ -42,3 +45,39 @@ export default function useColumns(columns: any[] = []) {
|
||||
opExtra,
|
||||
};
|
||||
}
|
||||
|
||||
interface EditRowProps {
|
||||
rowKey: string;
|
||||
row: Record<string, any>;
|
||||
actions: any;
|
||||
}
|
||||
export const EditRow: FC<EditRowProps> = ({ rowKey, row, actions }) => {
|
||||
const [isEdit, setEdit] = useState(false);
|
||||
const [val, setVal] = useState(row[rowKey]);
|
||||
const handleEdit = () => {
|
||||
setEdit(true);
|
||||
};
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setVal(e.target.value)
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
setEdit(false);
|
||||
row[rowKey] = val;
|
||||
actions?.setRecord(row, 'rowedit')
|
||||
};
|
||||
|
||||
return isEdit
|
||||
? (
|
||||
<Input.TextArea
|
||||
value={val}
|
||||
rows={1}
|
||||
onChange={handleChange}
|
||||
{...DISABLE_AUTO_COMPLETE}
|
||||
onPressEnter={handleSave}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<div className='rowedit' onClick={handleEdit}>{val}</div>
|
||||
);
|
||||
};
|
23
src/hooks/useJson.ts
vendored
Normal file
23
src/hooks/useJson.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { readJSON, writeJSON } from '@/utils';
|
||||
import useInit from '@/hooks/useInit';
|
||||
|
||||
export default function useJson<T>(file: string) {
|
||||
const [json, setData] = useState<T>();
|
||||
|
||||
const refreshJson = async () => {
|
||||
const data = await readJSON(file);
|
||||
setData(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateJson = async (data: any) => {
|
||||
await writeJSON(file, data);
|
||||
await refreshJson();
|
||||
};
|
||||
|
||||
useInit(refreshJson);
|
||||
|
||||
return { json, refreshJson, updateJson };
|
||||
}
|
33
src/hooks/useTable.tsx
vendored
33
src/hooks/useTable.tsx
vendored
@ -4,14 +4,35 @@ import type { TableRowSelection } from 'antd/es/table/interface';
|
||||
|
||||
import { safeKey } from '@/hooks/useData';
|
||||
|
||||
export default function useTableRowSelection() {
|
||||
type rowSelectionOptions = {
|
||||
key: 'id' | string;
|
||||
rowType: 'id' | 'row' | 'all';
|
||||
}
|
||||
export function useTableRowSelection(options: Partial<rowSelectionOptions> = {}) {
|
||||
const { key = 'id', rowType = 'id' } = options;
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [selectedRowIDs, setSelectedRowIDs] = useState<string[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Record<string|symbol, any>[]>([]);
|
||||
|
||||
const onSelectChange = (newSelectedRowKeys: React.Key[], selectedRows: Record<string|symbol, any>) => {
|
||||
const keys = selectedRows.map((i: any) => i[safeKey]);
|
||||
setSelectedRowIDs(keys);
|
||||
const onSelectChange = (newSelectedRowKeys: React.Key[], newSelectedRows: Record<string|symbol, any>[]) => {
|
||||
const keys = newSelectedRows.map((i: any) => i[safeKey] || i[key]);
|
||||
setSelectedRowKeys(newSelectedRowKeys);
|
||||
if (rowType === 'id') {
|
||||
setSelectedRowIDs(keys);
|
||||
}
|
||||
if (rowType === 'row') {
|
||||
setSelectedRows(newSelectedRows);
|
||||
}
|
||||
if (rowType === 'all') {
|
||||
setSelectedRowIDs(keys);
|
||||
setSelectedRows(newSelectedRows);
|
||||
}
|
||||
};
|
||||
|
||||
const rowReset = () => {
|
||||
setSelectedRowKeys([]);
|
||||
setSelectedRowIDs([]);
|
||||
setSelectedRows([]);
|
||||
};
|
||||
|
||||
const rowSelection: TableRowSelection<Record<string, any>> = {
|
||||
@ -24,14 +45,14 @@ export default function useTableRowSelection() {
|
||||
],
|
||||
};
|
||||
|
||||
return { rowSelection, selectedRowIDs };
|
||||
return { rowSelection, selectedRowIDs, selectedRows, rowReset };
|
||||
}
|
||||
|
||||
export const TABLE_PAGINATION = {
|
||||
hideOnSinglePage: true,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
defaultPageSize: 5,
|
||||
defaultPageSize: 10,
|
||||
pageSizeOptions: [5, 10, 15, 20],
|
||||
showTotal: (total: number) => <span>Total {total} items</span>,
|
||||
};
|
11
src/layout/index.tsx
vendored
11
src/layout/index.tsx
vendored
@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import {Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd';
|
||||
import { Layout, Menu, Tooltip, ConfigProvider, theme, Tag } from 'antd';
|
||||
import { SyncOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { getName, getVersion } from '@tauri-apps/api/app';
|
||||
@ -29,11 +29,13 @@ export default function ChatLayout() {
|
||||
await invoke('run_check_update', { silent: false, hasMsg: true });
|
||||
}
|
||||
|
||||
const isDark = appInfo.appTheme === "dark";
|
||||
|
||||
return (
|
||||
<ConfigProvider theme={{algorithm: appInfo.appTheme === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm}}>
|
||||
<ConfigProvider theme={{ algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm }}>
|
||||
<Layout style={{ minHeight: '100vh' }} hasSider>
|
||||
<Sider
|
||||
theme={appInfo.appTheme === "dark" ? "dark" : "light"}
|
||||
theme={isDark ? "dark" : "light"}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={(value) => setCollapsed(value)}
|
||||
@ -78,7 +80,8 @@ export default function ChatLayout() {
|
||||
<Routes />
|
||||
</Content>
|
||||
<Footer style={{ textAlign: 'center' }}>
|
||||
<a href="https://github.com/lencx/chatgpt" target="_blank">ChatGPT Desktop Application</a> ©2022 Created by lencx</Footer>
|
||||
<a href="https://github.com/lencx/chatgpt" target="_blank">ChatGPT Desktop Application</a> ©2022 Created by lencx
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</ConfigProvider>
|
||||
|
20
src/main.scss
vendored
20
src/main.scss
vendored
@ -31,10 +31,27 @@ html, body {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.ellipsis-line {
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rowedit {
|
||||
padding: 2px 5px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 2px rgba(237, 122, 60, 0.8);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-add-btn {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@ -51,6 +68,7 @@ html, body {
|
||||
}
|
||||
}
|
||||
|
||||
.chat-file-path,
|
||||
.chat-sync-path {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
27
src/routes.tsx
vendored
27
src/routes.tsx
vendored
@ -1,10 +1,12 @@
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
import {
|
||||
DesktopOutlined,
|
||||
SettingOutlined,
|
||||
BulbOutlined,
|
||||
SyncOutlined,
|
||||
FileSyncOutlined,
|
||||
UserOutlined,
|
||||
DownloadOutlined,
|
||||
FormOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { MenuProps } from 'antd';
|
||||
|
||||
@ -13,6 +15,8 @@ import UserCustom from '@/view/model/UserCustom';
|
||||
import SyncPrompts from '@/view/model/SyncPrompts';
|
||||
import SyncCustom from '@/view/model/SyncCustom';
|
||||
import SyncRecord from '@/view/model/SyncRecord';
|
||||
import Download from '@/view/download';
|
||||
import Notes from '@/view/notes';
|
||||
|
||||
export type ChatRouteMetaObject = {
|
||||
label: string;
|
||||
@ -33,7 +37,15 @@ export const routes: Array<ChatRouteObject> = [
|
||||
element: <General />,
|
||||
meta: {
|
||||
label: 'General',
|
||||
icon: <DesktopOutlined />,
|
||||
icon: <SettingOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/notes',
|
||||
element: <Notes />,
|
||||
meta: {
|
||||
label: 'Notes',
|
||||
icon: <FormOutlined />,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -51,6 +63,7 @@ export const routes: Array<ChatRouteObject> = [
|
||||
icon: <UserOutlined />,
|
||||
},
|
||||
},
|
||||
// --- Sync
|
||||
{
|
||||
path: 'sync-prompts',
|
||||
element: <SyncPrompts />,
|
||||
@ -72,7 +85,15 @@ export const routes: Array<ChatRouteObject> = [
|
||||
element: <SyncRecord />,
|
||||
hideMenu: true,
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'download',
|
||||
element: <Download />,
|
||||
meta: {
|
||||
label: 'Download',
|
||||
icon: <DownloadOutlined />,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
2
src/utils.ts
vendored
2
src/utils.ts
vendored
@ -4,6 +4,8 @@ import dayjs from 'dayjs';
|
||||
|
||||
export const CHAT_MODEL_JSON = 'chat.model.json';
|
||||
export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json';
|
||||
export const CHAT_DOWNLOAD_JSON = 'chat.download.json';
|
||||
export const CHAT_NOTES_JSON = 'chat.notes.json';
|
||||
export const CHAT_PROMPTS_CSV = 'chat.prompts.csv';
|
||||
export const GITHUB_PROMPTS_CSV_URL = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv';
|
||||
export const DISABLE_AUTO_COMPLETE = {
|
||||
|
80
src/view/download/config.tsx
vendored
Normal file
80
src/view/download/config.tsx
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
import { useState } from 'react';
|
||||
import { Tag, Space, Popconfirm } from 'antd';
|
||||
import { path, shell } from '@tauri-apps/api';
|
||||
|
||||
import { EditRow } from '@/hooks/useColumns';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import { fmtDate, chatRoot } from '@/utils';
|
||||
|
||||
const colorMap: any = {
|
||||
pdf: 'blue',
|
||||
png: 'orange',
|
||||
}
|
||||
|
||||
export const downloadColumns = () => [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
fixed: 'left',
|
||||
key: 'name',
|
||||
width: 240,
|
||||
render: (_: string, row: any, actions: any) => (
|
||||
<EditRow rowKey="name" row={row} actions={actions} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Extension',
|
||||
dataIndex: 'ext',
|
||||
key: 'ext',
|
||||
width: 120,
|
||||
render: (v: string) => <Tag color={colorMap[v]}>{v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'Path',
|
||||
dataIndex: 'path',
|
||||
key: 'path',
|
||||
width: 200,
|
||||
render: (_: string, row: any) => <RenderPath row={row} />,
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
dataIndex: 'created',
|
||||
key: 'created',
|
||||
width: 150,
|
||||
render: fmtDate,
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
fixed: 'right',
|
||||
width: 150,
|
||||
render: (_: any, row: any, actions: any) => {
|
||||
return (
|
||||
<Space>
|
||||
<a onClick={() => actions.setRecord(row, 'preview')}>Preview</a>
|
||||
<Popconfirm
|
||||
title="Are you sure to delete this file?"
|
||||
onConfirm={() => actions.setRecord(row, 'delete')}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<a>Delete</a>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const RenderPath = ({ row }: any) => {
|
||||
const [filePath, setFilePath] = useState('');
|
||||
useInit(async () => {
|
||||
setFilePath(await getPath(row));
|
||||
})
|
||||
return <a onClick={() => shell.open(filePath)}>{filePath}</a>;
|
||||
};
|
||||
|
||||
export const getPath = async (row: any) => {
|
||||
const isImg = ['png'].includes(row?.ext);
|
||||
return await path.join(await chatRoot(), 'download', isImg ? 'img' : row.ext, row.id) + `.${row.ext}`;
|
||||
}
|
145
src/view/download/index.tsx
vendored
Normal file
145
src/view/download/index.tsx
vendored
Normal file
@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Modal, Popconfirm, Button, message } from 'antd';
|
||||
import { invoke, path, shell, fs } from '@tauri-apps/api';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import useJson from '@/hooks/useJson';
|
||||
import useData from '@/hooks/useData';
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils';
|
||||
import { downloadColumns } from './config';
|
||||
|
||||
function renderFile(buff: Uint8Array, type: string) {
|
||||
const renderType = {
|
||||
pdf: 'application/pdf',
|
||||
png: 'image/png',
|
||||
}[type];
|
||||
return URL.createObjectURL(new Blob([buff], { type: renderType }));
|
||||
}
|
||||
|
||||
export default function Download() {
|
||||
const [downloadPath, setDownloadPath] = useState('');
|
||||
const [source, setSource] = useState('');
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const { opData, opInit, opReplace, opSafeKey } = useData([]);
|
||||
const { columns, ...opInfo } = useColumns(downloadColumns());
|
||||
const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' });
|
||||
const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_DOWNLOAD_JSON);
|
||||
const selectedItems = rowSelection.selectedRowKeys || [];
|
||||
|
||||
useInit(async () => {
|
||||
const file = await path.join(await chatRoot(), CHAT_DOWNLOAD_JSON);
|
||||
setDownloadPath(file);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!json || json.length <= 0) return;
|
||||
opInit(json);
|
||||
}, [json?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opInfo.opType) return;
|
||||
(async () => {
|
||||
const record = opInfo?.opRecord;
|
||||
const isImg = ['png'].includes(record?.ext);
|
||||
const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : record?.ext, `${record?.id}.${record?.ext}`);
|
||||
if (opInfo.opType === 'preview') {
|
||||
const data = await fs.readBinaryFile(file);
|
||||
const sourceData = renderFile(data, record?.ext);
|
||||
setSource(sourceData);
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
if (opInfo.opType === 'delete') {
|
||||
await fs.removeFile(file);
|
||||
await handleRefresh();
|
||||
}
|
||||
if (opInfo.opType === 'rowedit') {
|
||||
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
|
||||
await updateJson(data);
|
||||
message.success('Name has been changed!');
|
||||
}
|
||||
opInfo.resetRecord();
|
||||
})()
|
||||
}, [opInfo.opType])
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (opData?.length === selectedRows.length) {
|
||||
const downloadDir = await path.join(await chatRoot(), 'download');
|
||||
await fs.removeDir(downloadDir, { recursive: true });
|
||||
await handleRefresh();
|
||||
rowReset();
|
||||
message.success('All files have been cleared!');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = selectedRows.map(async (i) => {
|
||||
const isImg = ['png'].includes(i?.ext);
|
||||
const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : i?.ext, `${i?.id}.${i?.ext}`);
|
||||
await fs.removeFile(file);
|
||||
return file;
|
||||
})
|
||||
Promise.all(rows).then(async () => {
|
||||
await handleRefresh();
|
||||
message.success('All files selected are cleared!');
|
||||
});
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON, dir: 'download' });
|
||||
const data = await refreshJson();
|
||||
opInit(data);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setVisible(false);
|
||||
opInfo.resetRecord();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="chat-table-btns">
|
||||
<div>
|
||||
{selectedItems.length > 0 && (
|
||||
<>
|
||||
<Popconfirm
|
||||
overlayStyle={{ width: 250 }}
|
||||
title="Files cannot be recovered after deletion, are you sure you want to delete them?"
|
||||
placement="topLeft"
|
||||
onConfirm={handleDelete}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<Button>Batch delete</Button>
|
||||
</Popconfirm>
|
||||
<span className="num">Selected {selectedItems.length} items</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-table-tip">
|
||||
<div className="chat-file-path">
|
||||
<div>PATH: <a onClick={() => shell.open(downloadPath)} title={downloadPath}>{downloadPath}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
scroll={{ x: 800 }}
|
||||
dataSource={opData}
|
||||
rowSelection={rowSelection}
|
||||
pagination={TABLE_PAGINATION}
|
||||
/>
|
||||
<Modal
|
||||
open={isVisible}
|
||||
title={<div>{opInfo?.opRecord?.name || ''}</div>}
|
||||
onCancel={handleCancel}
|
||||
footer={false}
|
||||
destroyOnClose
|
||||
>
|
||||
<img style={{ maxWidth: '100%' }} src={source} />
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
5
src/view/model/SyncPrompts/config.tsx
vendored
5
src/view/model/SyncPrompts/config.tsx
vendored
@ -1,4 +1,4 @@
|
||||
import { Switch, Tag, Tooltip } from 'antd';
|
||||
import { Table, Switch, Tag } from 'antd';
|
||||
|
||||
import { genCmd } from '@/utils';
|
||||
|
||||
@ -35,13 +35,14 @@ export const syncColumns = () => [
|
||||
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
|
||||
),
|
||||
},
|
||||
Table.EXPAND_COLUMN,
|
||||
{
|
||||
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>
|
||||
<span className="chat-prompts-val">{v}</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
5
src/view/model/SyncPrompts/index.tsx
vendored
5
src/view/model/SyncPrompts/index.tsx
vendored
@ -6,7 +6,7 @@ import useInit from '@/hooks/useInit';
|
||||
import useData from '@/hooks/useData';
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
|
||||
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { fmtDate, chatRoot } from '@/utils';
|
||||
import { syncColumns } from './config';
|
||||
import './index.scss';
|
||||
@ -14,7 +14,7 @@ import './index.scss';
|
||||
const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv';
|
||||
|
||||
export default function SyncPrompts() {
|
||||
const { rowSelection, selectedRowIDs } = useTable();
|
||||
const { rowSelection, selectedRowIDs } = useTableRowSelection();
|
||||
const [jsonPath, setJsonPath] = useState('');
|
||||
const { modelJson, modelSet } = useChatModel('sync_prompts');
|
||||
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
|
||||
@ -93,6 +93,7 @@ export default function SyncPrompts() {
|
||||
dataSource={opData}
|
||||
rowSelection={rowSelection}
|
||||
pagination={TABLE_PAGINATION}
|
||||
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
5
src/view/model/SyncRecord/config.tsx
vendored
5
src/view/model/SyncRecord/config.tsx
vendored
@ -1,4 +1,4 @@
|
||||
import { Switch, Tag, Tooltip } from 'antd';
|
||||
import { Switch, Tag, Table } from 'antd';
|
||||
|
||||
import { genCmd } from '@/utils';
|
||||
|
||||
@ -37,13 +37,14 @@ export const syncColumns = () => [
|
||||
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
|
||||
),
|
||||
},
|
||||
Table.EXPAND_COLUMN,
|
||||
{
|
||||
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>
|
||||
<span className="chat-prompts-val">{v}</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
5
src/view/model/SyncRecord/index.tsx
vendored
5
src/view/model/SyncRecord/index.tsx
vendored
@ -7,7 +7,7 @@ import { shell, path } from '@tauri-apps/api';
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import useData from '@/hooks/useData';
|
||||
import { useCacheModel } from '@/hooks/useChatModel';
|
||||
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { fmtDate, chatRoot } from '@/utils';
|
||||
import { getPath } from '@/view/model/SyncCustom/config';
|
||||
import { syncColumns } from './config';
|
||||
@ -19,7 +19,7 @@ export default function SyncRecord() {
|
||||
const [jsonPath, setJsonPath] = useState('');
|
||||
const state = location?.state;
|
||||
|
||||
const { rowSelection, selectedRowIDs } = useTable();
|
||||
const { rowSelection, selectedRowIDs } = useTableRowSelection();
|
||||
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
|
||||
const { opData, opInit, opReplace, opReplaceItems, opSafeKey } = useData([]);
|
||||
const { columns, ...opInfo } = useColumns(syncColumns());
|
||||
@ -79,6 +79,7 @@ export default function SyncRecord() {
|
||||
dataSource={opData}
|
||||
rowSelection={rowSelection}
|
||||
pagination={TABLE_PAGINATION}
|
||||
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
5
src/view/model/UserCustom/config.tsx
vendored
5
src/view/model/UserCustom/config.tsx
vendored
@ -1,4 +1,4 @@
|
||||
import { Tag, Switch, Tooltip, Space, Popconfirm } from 'antd';
|
||||
import { Tag, Switch, Space, Popconfirm, Table } from 'antd';
|
||||
|
||||
export const modelColumns = () => [
|
||||
{
|
||||
@ -33,13 +33,14 @@ export const modelColumns = () => [
|
||||
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
|
||||
),
|
||||
},
|
||||
Table.EXPAND_COLUMN,
|
||||
{
|
||||
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>
|
||||
<span className="chat-prompts-val">{v}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
5
src/view/model/UserCustom/index.tsx
vendored
5
src/view/model/UserCustom/index.tsx
vendored
@ -6,13 +6,13 @@ import useInit from '@/hooks/useInit';
|
||||
import useData from '@/hooks/useData';
|
||||
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { chatRoot, fmtDate } from '@/utils';
|
||||
import { modelColumns } from './config';
|
||||
import UserCustomForm from './Form';
|
||||
|
||||
export default function LanguageModel() {
|
||||
const { rowSelection, selectedRowIDs } = useTable();
|
||||
const { rowSelection, selectedRowIDs } = useTableRowSelection();
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const [jsonPath, setJsonPath] = useState('');
|
||||
const { modelJson, modelSet } = useChatModel('user_custom');
|
||||
@ -123,6 +123,7 @@ export default function LanguageModel() {
|
||||
dataSource={opData}
|
||||
rowSelection={rowSelection}
|
||||
pagination={TABLE_PAGINATION}
|
||||
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}}
|
||||
/>
|
||||
<Modal
|
||||
open={isVisible}
|
||||
|
69
src/view/notes/config.tsx
vendored
Normal file
69
src/view/notes/config.tsx
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
import { useState } from 'react';
|
||||
import { Space, Popconfirm } from 'antd';
|
||||
import { path, shell } from '@tauri-apps/api';
|
||||
|
||||
import { EditRow } from '@/hooks/useColumns';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import { fmtDate, chatRoot } from '@/utils';
|
||||
|
||||
export const notesColumns = () => [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
fixed: 'left',
|
||||
key: 'name',
|
||||
width: 240,
|
||||
render: (_: string, row: any, actions: any) => (
|
||||
<EditRow rowKey="name" row={row} actions={actions} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Path',
|
||||
dataIndex: 'path',
|
||||
key: 'path',
|
||||
width: 200,
|
||||
render: (_: string, row: any) => <RenderPath row={row} />,
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
dataIndex: 'created',
|
||||
key: 'created',
|
||||
width: 150,
|
||||
render: fmtDate,
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
fixed: 'right',
|
||||
width: 160,
|
||||
render: (_: any, row: any, actions: any) => {
|
||||
return (
|
||||
<Space>
|
||||
<a onClick={() => actions.setRecord(row, 'preview')}>Preview</a>
|
||||
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
|
||||
<Popconfirm
|
||||
title="Are you sure to delete this file?"
|
||||
onConfirm={() => actions.setRecord(row, 'delete')}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<a>Delete</a>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const RenderPath = ({ row }: any) => {
|
||||
const [filePath, setFilePath] = useState('');
|
||||
useInit(async () => {
|
||||
setFilePath(await getPath(row));
|
||||
})
|
||||
return <a onClick={() => shell.open(filePath)}>{filePath}</a>;
|
||||
};
|
||||
|
||||
export const getPath = async (row: any) => {
|
||||
const isImg = ['png'].includes(row?.ext);
|
||||
return await path.join(await chatRoot(), 'notes', row.id) + `.${row.ext}`;
|
||||
}
|
160
src/view/notes/index.tsx
vendored
Normal file
160
src/view/notes/index.tsx
vendored
Normal file
@ -0,0 +1,160 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Table, Modal, Popconfirm, Button, message } from 'antd';
|
||||
import { invoke, path, shell, fs } from '@tauri-apps/api';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { a11yDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
import useInit from '@/hooks/useInit';
|
||||
import useJson from '@/hooks/useJson';
|
||||
import useData from '@/hooks/useData';
|
||||
import useColumns from '@/hooks/useColumns';
|
||||
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
|
||||
import { chatRoot, CHAT_NOTES_JSON } from '@/utils';
|
||||
import { notesColumns } from './config';
|
||||
|
||||
export default function Notes() {
|
||||
const [notesPath, setNotesPath] = useState('');
|
||||
const [source, setSource] = useState('');
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
const { opData, opInit, opReplace, opSafeKey } = useData([]);
|
||||
const { columns, ...opInfo } = useColumns(notesColumns());
|
||||
const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' });
|
||||
const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_NOTES_JSON);
|
||||
const selectedItems = rowSelection.selectedRowKeys || [];
|
||||
|
||||
useInit(async () => {
|
||||
const file = await path.join(await chatRoot(), CHAT_NOTES_JSON);
|
||||
setNotesPath(file);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!json || json.length <= 0) return;
|
||||
opInit(json);
|
||||
}, [json?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!opInfo.opType) return;
|
||||
(async () => {
|
||||
const record = opInfo?.opRecord;
|
||||
const file = await path.join(await chatRoot(), 'notes', `${record?.id}.${record?.ext}`);
|
||||
if (opInfo.opType === 'preview') {
|
||||
const data = await fs.readTextFile(file);
|
||||
setSource(data);
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
if (opInfo.opType === 'edit') {
|
||||
alert('TODO');
|
||||
}
|
||||
if (opInfo.opType === 'delete') {
|
||||
await fs.removeFile(file);
|
||||
await handleRefresh();
|
||||
}
|
||||
if (opInfo.opType === 'rowedit') {
|
||||
const data = opReplace(opInfo?.opRecord?.[opSafeKey], opInfo?.opRecord);
|
||||
await updateJson(data);
|
||||
message.success('Name has been changed!');
|
||||
}
|
||||
opInfo.resetRecord();
|
||||
})()
|
||||
}, [opInfo.opType])
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (opData?.length === selectedRows.length) {
|
||||
const notesDir = await path.join(await chatRoot(), 'notes');
|
||||
await fs.removeDir(notesDir, { recursive: true });
|
||||
await handleRefresh();
|
||||
rowReset();
|
||||
message.success('All files have been cleared!');
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = selectedRows.map(async (i) => {
|
||||
const file = await path.join(await chatRoot(), 'notes', `${i?.id}.${i?.ext}`);
|
||||
await fs.removeFile(file);
|
||||
return file;
|
||||
})
|
||||
Promise.all(rows).then(async () => {
|
||||
await handleRefresh();
|
||||
message.success('All files selected are cleared!');
|
||||
});
|
||||
};
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await invoke('download_list', { pathname: CHAT_NOTES_JSON, dir: 'notes' });
|
||||
const data = await refreshJson();
|
||||
opInit(data);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setVisible(false);
|
||||
opInfo.resetRecord();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="chat-table-btns">
|
||||
<div>
|
||||
{selectedItems.length > 0 && (
|
||||
<>
|
||||
<Popconfirm
|
||||
overlayStyle={{ width: 250 }}
|
||||
title="Files cannot be recovered after deletion, are you sure you want to delete them?"
|
||||
placement="topLeft"
|
||||
onConfirm={handleDelete}
|
||||
okText="Yes"
|
||||
cancelText="No"
|
||||
>
|
||||
<Button>Batch delete</Button>
|
||||
</Popconfirm>
|
||||
<span className="num">Selected {selectedItems.length} items</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="chat-table-tip">
|
||||
<div className="chat-file-path">
|
||||
<div>PATH: <a onClick={() => shell.open(notesPath)} title={notesPath}>{notesPath}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
scroll={{ x: 800 }}
|
||||
dataSource={opData}
|
||||
rowSelection={rowSelection}
|
||||
pagination={TABLE_PAGINATION}
|
||||
/>
|
||||
<Modal
|
||||
open={isVisible}
|
||||
title={<div>{opInfo?.opRecord?.name || ''}</div>}
|
||||
onCancel={handleCancel}
|
||||
footer={false}
|
||||
destroyOnClose
|
||||
>
|
||||
<ReactMarkdown
|
||||
children={source}
|
||||
components={{
|
||||
code({node, inline, className, children, ...props}) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
children={String(children).replace(/\n$/, '')}
|
||||
style={a11yDark as any}
|
||||
language={match[1]}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user