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

refactor: prompts

This commit is contained in:
lencx 2023-05-23 01:33:24 +08:00
parent c53524b472
commit 882593479b
7 changed files with 309 additions and 254 deletions

View File

@ -5,22 +5,13 @@
</p> </p>
[![English badge](https://img.shields.io/badge/%E8%8B%B1%E6%96%87-English-blue)](./README.md) [![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)
![visitor](https://visitor-badge.glitch.me/badge?page_id=lencx.chatgpt)
[![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases) [![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) [![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_) [![lencx](https://img.shields.io/badge/follow-lencx__-blue?style=flat&logo=Twitter)](https://twitter.com/lencx_)
<!-- [![lencx](https://img.shields.io/twitter/follow/lencx_.svg?style=social)](https://twitter.com/lencx_) -->
<!-- [![中文版 badge](https://img.shields.io/badge/%E4%B8%AD%E6%96%87-Traditional%20Chinese-blue)](./README-ZH.md) -->
<a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 145px !important;" ></a> <a href="https://www.buymeacoffee.com/lencx" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-blue.png" alt="Buy Me A Coffee" style="height: 40px !important;width: 145px !important;" ></a>
**🛑 URGENT NOTICE: A hacker has been found to take advantage of the heat of `lencx/ChatGPT` to plant a Trojan horse after the fork project and rebuild the installer. If you have friends around you who are using this desktop application, please remind them not to download unknown links freely. Now the project will remove other installation ways and only provide this download link https://github.com/lencx/ChatGPT/releases**
**🛑 紧急通知:目前发现有黑客利用 `lencx/ChatGPT` 的热度,在 fork 项目后植入木马,重新构建安装程序。如果你身边有朋友正在使用此桌面应用,请提醒 TA 们不要随意下载不明链接。现在项目将删除其他安装途径,仅提供此下载链接 https://github.com/lencx/ChatGPT/releases**
--- ---
**It is an unofficial project intended for personal learning and research purposes only. During the time that the ChatGPT desktop application was open-sourced, it received a lot of attention, and I would like to thank everyone for their support. However, as things have developed, there are two issues that seriously affect the project's next development plan:** **It is an unofficial project intended for personal learning and research purposes only. During the time that the ChatGPT desktop application was open-sourced, it received a lot of attention, and I would like to thank everyone for their support. However, as things have developed, there are two issues that seriously affect the project's next development plan:**

View File

@ -9,6 +9,8 @@ use std::{collections::HashMap, fs, path::PathBuf, vec};
use tauri::{api, command, AppHandle, Manager}; use tauri::{api, command, AppHandle, Manager};
use walkdir::WalkDir; use walkdir::WalkDir;
use super::fs_extra::Error;
#[command] #[command]
pub fn get_chat_prompt_cmd() -> serde_json::Value { pub fn get_chat_prompt_cmd() -> serde_json::Value {
let path = utils::app_root().join("chat.prompt.cmd.json"); let path = utils::app_root().join("chat.prompt.cmd.json");
@ -24,23 +26,28 @@ pub struct PromptBaseRecord {
} }
#[command] #[command]
pub fn parse_prompt(data: String) -> Vec<PromptBaseRecord> { pub fn parse_prompt(data: String) -> Option<Vec<PromptBaseRecord>> {
let mut rdr = csv::Reader::from_reader(data.as_bytes()); let mut rdr = csv::Reader::from_reader(data.as_bytes());
let mut list = vec![]; let mut list = vec![];
for result in rdr.deserialize() {
let record: PromptBaseRecord = result.unwrap_or_else(|err| { for result in rdr.deserialize::<PromptBaseRecord>() {
error!("parse_prompt: {}", err); match result {
PromptBaseRecord { Ok(record) => {
cmd: None, if !record.act.is_empty() {
act: "".to_string(), list.push(record);
prompt: "".to_string(), }
}
Err(err) => {
error!("parse_prompt: {}", err);
} }
});
if !record.act.is_empty() {
list.push(record);
} }
} }
list
if list.is_empty() {
None
} else {
Some(list)
}
} }
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
@ -165,87 +172,89 @@ pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<PromptRecord>
.unwrap(); .unwrap();
if let Some(v) = res { if let Some(v) = res {
let data = parse_prompt(v) if let Some(data) = parse_prompt(v) {
.iter() let transformed_data = data
.map(move |i| PromptRecord { .iter()
cmd: if i.cmd.is_some() { .map(|i| PromptRecord {
i.cmd.clone().unwrap() cmd: if let Some(cmd) = &i.cmd {
} else { cmd.clone()
utils::gen_cmd(i.act.clone()) } else {
}, utils::gen_cmd(i.act.clone())
act: i.act.clone(), },
prompt: i.prompt.clone(), act: i.act.clone(),
tags: vec!["chatgpt-prompts".to_string()], prompt: i.prompt.clone(),
enable: true, tags: vec!["chatgpt-prompts".to_string()],
}) enable: true,
.collect::<Vec<PromptRecord>>();
let data2 = data.clone();
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()
.join("cache_prompts")
.join("chatgpt_prompts.json");
if !utils::exists(&prompts) {
fs::write(
&prompts,
serde_json::json!({
"name": "ChatGPT Prompts",
"link": "https://github.com/lencx/ChatGPT"
}) })
.to_string(), .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()
.join("cache_prompts")
.join("chatgpt_prompts.json");
if !utils::exists(&prompts) {
fs::write(
&prompts,
serde_json::json!({
"name": "ChatGPT Prompts",
"link": "https://github.com/lencx/ChatGPT"
})
.to_string(),
)
.unwrap();
}
// chatgpt_prompts.json
fs::write(
chatgpt_prompts,
serde_json::to_string_pretty(&data).unwrap(),
) )
.unwrap(); .unwrap();
let cmd_data = cmd_list();
// chat.prompt.cmd.json
fs::write(
prompt_cmd,
serde_json::to_string_pretty(&serde_json::json!({
"name": "ChatGPT CMD",
"last_updated": time,
"data": cmd_data,
}))
.unwrap(),
)
.unwrap();
let mut kv = HashMap::new();
kv.insert(
"sync_prompts".to_string(),
serde_json::json!({ "id": "chatgpt_prompts", "last_updated": time }),
);
let prompts_data = utils::merge(
&serde_json::from_str(&fs::read_to_string(&prompts).unwrap()).unwrap(),
&kv,
);
// chat.prompt.json
fs::write(
prompts,
serde_json::to_string_pretty(&prompts_data).unwrap(),
)
.unwrap();
// refresh window
api::dialog::message(
app.get_window("core").as_ref(),
"Sync Prompts",
"Prompts data has been synchronized!",
);
window::cmd::window_reload(app.clone(), "core");
window::cmd::window_reload(app, "tray");
return Some(data2);
} }
// chatgpt_prompts.json
fs::write(
chatgpt_prompts,
serde_json::to_string_pretty(&data).unwrap(),
)
.unwrap();
let cmd_data = cmd_list();
// chat.prompt.cmd.json
fs::write(
prompt_cmd,
serde_json::to_string_pretty(&serde_json::json!({
"name": "ChatGPT CMD",
"last_updated": time,
"data": cmd_data,
}))
.unwrap(),
)
.unwrap();
let mut kv = HashMap::new();
kv.insert(
"sync_prompts".to_string(),
serde_json::json!({ "id": "chatgpt_prompts", "last_updated": time }),
);
let prompts_data = utils::merge(
&serde_json::from_str(&fs::read_to_string(&prompts).unwrap()).unwrap(),
&kv,
);
// chat.prompt.json
fs::write(
prompts,
serde_json::to_string_pretty(&prompts_data).unwrap(),
)
.unwrap();
// refresh window
api::dialog::message(
app.get_window("core").as_ref(),
"Sync Prompts",
"ChatGPT Prompts data has been synchronized!",
);
window::cmd::window_reload(app.clone(), "core");
window::cmd::window_reload(app, "tray");
return Some(data2);
} }
None None
@ -260,37 +269,40 @@ pub async fn sync_user_prompts(url: String, data_type: String) -> Option<Vec<Pro
}); });
if let Some(v) = res { if let Some(v) = res {
let data; let data: Option<Vec<PromptBaseRecord>> = if data_type == "csv" {
if data_type == "csv" {
info!("chatgpt_http_csv_parse"); info!("chatgpt_http_csv_parse");
data = parse_prompt(v); parse_prompt(v)
} else if data_type == "json" { } else if data_type == "json" {
info!("chatgpt_http_json_parse"); info!("chatgpt_http_json_parse");
data = serde_json::from_str(&v).unwrap_or_else(|err| { match serde_json::from_str::<Vec<PromptBaseRecord>>(&v) {
error!("chatgpt_http_json_parse: {}", err); Ok(parsed) => Some(parsed),
vec![] Err(err) => {
}); error!("chatgpt_http_json_parse: {}", err);
None
}
}
} else { } else {
error!("chatgpt_http_unknown_type"); error!("chatgpt_http_unknown_type");
data = vec![]; None
};
if let Some(base_records) = data {
let data = base_records
.iter()
.map(|i| PromptRecord {
cmd: i
.cmd
.clone()
.unwrap_or_else(|| utils::gen_cmd(i.act.clone())),
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["user-sync".to_string()],
enable: true,
})
.collect::<Vec<PromptRecord>>();
return Some(data);
} }
let data = data
.iter()
.map(move |i| PromptRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
} else {
utils::gen_cmd(i.act.clone())
},
act: i.act.clone(),
prompt: i.prompt.clone(),
tags: vec!["user-sync".to_string()],
enable: true,
})
.collect::<Vec<PromptRecord>>();
return Some(data);
} }
None None

View File

@ -18,11 +18,21 @@ export default function useChatPrompt(key: string, file = CHAT_PROMPT_JSON) {
const promptSet = async (data: Record<string, any>[] | Record<string, any>) => { const promptSet = async (data: Record<string, any>[] | Record<string, any>) => {
const oData = clone(promptJson); const oData = clone(promptJson);
oData[key] = data; oData[key] = data;
await writeJSON(file, oData); await writeJSON(file, oData);
setPromptJson(oData); setPromptJson(oData);
}; };
return { promptJson, promptSet, promptData: promptJson?.[key] || [] }; const promptUpdate = async (id: string, field: string, value: any) => {
const oData = clone(promptJson);
const idx = oData[key].findIndex((v: any) => v.id === id);
oData[key][idx][field] = value;
await writeJSON(file, oData);
setPromptJson(oData);
};
return { promptJson, promptSet, promptUpdate, promptData: promptJson?.[key] || [] };
} }
export function useCachePrompt(file = '') { export function useCachePrompt(file = '') {

View File

@ -5,17 +5,15 @@ import {
useImperativeHandle, useImperativeHandle,
forwardRef, forwardRef,
} from 'react'; } from 'react';
import { Form, Input, Radio, Upload, Tooltip, message } from 'antd'; import { Form, Input, Radio, Upload, Tooltip, Button, message } from 'antd';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { InboxOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
import type { FormProps, RadioChangeEvent, UploadProps, UploadFile } from 'antd'; import type { FormProps, RadioChangeEvent, UploadProps, UploadFile } from 'antd';
import { DISABLE_AUTO_COMPLETE, chatRoot } from '@/utils'; import { DISABLE_AUTO_COMPLETE, chatRoot } from '@/utils';
// import useInit from '@/hooks/useInit';
interface SyncFormProps { interface SyncFormProps {
record?: Record<string | symbol, any> | null; record?: Record<string | symbol, any> | null;
type: string;
} }
const initFormValue = { const initFormValue = {
@ -25,43 +23,18 @@ const initFormValue = {
protocol: 'https', protocol: 'https',
}; };
const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record, type }, ref) => { const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record }, ref) => {
// const isDisabled = type === 'edit';
const [form] = Form.useForm(); const [form] = Form.useForm();
useImperativeHandle(ref, () => ({ form })); useImperativeHandle(ref, () => ({ form }));
// const [root, setRoot] = useState('');
const [protocol, setProtocol] = useState('https'); const [protocol, setProtocol] = useState('https');
const [fileList, setFileList] = useState<UploadFile[]>([]); const [fileList, setFileList] = useState<UploadFile[]>([]);
// useInit(async () => {
// setRoot(await chatRoot());
// });
useEffect(() => { useEffect(() => {
if (record) { if (record) {
form.setFieldsValue(record); form.setFieldsValue(record);
} }
}, [record]); }, [record]);
// const pathOptions = (
// <Form.Item noStyle name="protocol" initialValue="https">
// <Select disabled={isDisabled}>
// <Select.Option value="local">{root}</Select.Option>
// <Select.Option value="http">http://</Select.Option>
// <Select.Option value="https">https://</Select.Option>
// </Select>
// </Form.Item>
// );
// const extOptions = (
// <Form.Item noStyle name="ext" initialValue="json">
// <Select disabled={isDisabled}>
// <Select.Option value="csv">.csv</Select.Option>
// <Select.Option value="json">.json</Select.Option>
// </Select>
// </Form.Item>
// );
const jsonTip = ( const jsonTip = (
<Tooltip <Tooltip
title={ title={
@ -135,49 +108,50 @@ const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record,
<Radio value="local">local</Radio> <Radio value="local">local</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<div style={{ marginLeft: 30, color: '#888' }}>
<p>
<b>.ext</b>: The file supports only {csvTip} and {jsonTip} formats.
</p>
</div>
{['http', 'https'].includes(protocol) && ( {['http', 'https'].includes(protocol) && (
<Form.Item <div style={{ height: 180 }}>
label="URL" <Form.Item
name="url" label="URL"
rules={[ name="url"
{ required: true, message: 'Please enter the URL!' }, rules={[
({ getFieldValue }) => ({ { required: true, message: 'Please enter the URL!' },
validator(_, value) { ({ getFieldValue }) => ({
if (!value || /\.json$|\.csv$/.test(getFieldValue('url'))) { validator(_, value) {
return Promise.resolve(); if (!value || /\.json$|\.csv$/.test(getFieldValue('url'))) {
} return Promise.resolve();
return Promise.reject(new Error('The file supports only .csv and .json formats')); }
}, return Promise.reject(
}), new Error('The file supports only .csv and .json formats'),
]} );
style={{ height: 200 }} },
> }),
<Input ]}
placeholder="your_path/file_name.ext" >
addonBefore={`${protocol}://`} <Input
// addonAfter={extOptions} placeholder="your_path/file_name.ext"
{...DISABLE_AUTO_COMPLETE} addonBefore={`${protocol}://`}
/> {...DISABLE_AUTO_COMPLETE}
</Form.Item> />
</Form.Item>
<div style={{ marginLeft: 80, color: '#888' }}>
<p>
<b>.ext</b>: only {csvTip} or {jsonTip} file formats are supported.
</p>
</div>
</div>
)} )}
{protocol === 'local' && ( {protocol === 'local' && (
<Form.Item <Form.Item
name="file" name="file"
label="File" label="File"
rules={[{ required: true, message: 'Please select a file!' }]} rules={[{ required: true, message: 'Please select a file!' }]}
style={{ height: 200 }} style={{ height: 168 }}
> >
<Upload.Dragger {...uploadOptions}> <Upload.Dragger {...uploadOptions}>
<p className="ant-upload-drag-icon"> <Button icon={<UploadOutlined />}>Click to Upload</Button>
<InboxOutlined /> <p className="ant-upload-hint">
Only {csvTip} or {jsonTip} file formats are supported.
</p> </p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
<p className="ant-upload-hint">Only .json or .csv files are supported.</p>
</Upload.Dragger> </Upload.Dragger>
</Form.Item> </Form.Item>
)} )}

View File

@ -4,6 +4,7 @@ import { HistoryOutlined } from '@ant-design/icons';
import { shell, path } from '@tauri-apps/api'; import { shell, path } from '@tauri-apps/api';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { EditRow } from '@/hooks/useColumns';
import useInit from '@/hooks/useInit'; import useInit from '@/hooks/useInit';
import { chatRoot, fmtDate } from '@/utils'; import { chatRoot, fmtDate } from '@/utils';
@ -13,6 +14,9 @@ export const syncColumns = () => [
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 100, width: 100,
render: (_: string, row: any, actions: any) => (
<EditRow rowKey="name" row={row} actions={actions} />
),
}, },
{ {
title: 'Protocol', title: 'Protocol',
@ -47,21 +51,22 @@ export const syncColumns = () => [
render: (_: any, row: any, actions: any) => { render: (_: any, row: any, actions: any) => {
return ( return (
<Space> <Space>
<Popconfirm {row.protocol !== 'local' && (
overlayStyle={{ width: 250 }} <Popconfirm
title="Sync will overwrite the previous data, confirm to sync?" overlayStyle={{ width: 250 }}
onConfirm={() => actions.setRecord(row, 'sync')} title="Sync will overwrite the previous data, confirm to sync?"
okText="Yes" onConfirm={() => actions.setRecord(row, 'sync')}
cancelText="No" okText="Yes"
> cancelText="No"
<a>Sync</a> >
</Popconfirm> <a>Sync</a>
</Popconfirm>
)}
{row.last_updated && ( {row.last_updated && (
<Link to={`${row.id}`} state={row}> <Link to={`${row.id}`} state={row}>
View View
</Link> </Link>
)} )}
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
<Popconfirm <Popconfirm
title="Are you sure to delete this path?" title="Are you sure to delete this path?"
onConfirm={() => actions.setRecord(row, 'delete')} onConfirm={() => actions.setRecord(row, 'delete')}
@ -86,8 +91,8 @@ const RenderPath = ({ row }: any) => {
export const getPath = async (row: any) => { export const getPath = async (row: any) => {
if (!/^http/.test(row.protocol)) { if (!/^http/.test(row.protocol)) {
return (await path.join(await chatRoot(), row.path)) + `.${row.ext}`; return await path.join(await chatRoot(), 'cache_prompts', `${row.id}.json`);
} else { } else {
return `${row.protocol}://${row.path}.${row.ext}`; return `${row.protocol}://${row.url}`;
} }
}; };

View File

@ -1,12 +1,13 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Table, Modal, Button, message } from 'antd'; import { Table, Modal, Button, message } from 'antd';
import { invoke, path, fs } from '@tauri-apps/api'; import { invoke, path, fs, shell } from '@tauri-apps/api';
import useData from '@/hooks/useData'; import useData from '@/hooks/useData';
import useInit from '@/hooks/useInit';
import useColumns from '@/hooks/useColumns'; import useColumns from '@/hooks/useColumns';
import { TABLE_PAGINATION } from '@/hooks/useTable'; import { TABLE_PAGINATION } from '@/hooks/useTable';
import useChatPrompt, { useCachePrompt } from '@/hooks/useChatPrompt'; import useChatPrompt, { useCachePrompt } from '@/hooks/useChatPrompt';
import { CHAT_PROMPT_JSON, chatRoot, readJSON, genCmd } from '@/utils'; import { CHAT_PROMPT_JSON, chatRoot, genCmd } from '@/utils';
import { syncColumns, getPath } from './config'; import { syncColumns, getPath } from './config';
import SyncForm from './Form'; import SyncForm from './Form';
@ -19,8 +20,9 @@ const fmtData = (data: Record<string, any>[] = []) =>
})); }));
export default function SyncCustom() { export default function SyncCustom() {
const [logPath, setLogPath] = useState('');
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
const { promptData, promptSet } = useChatPrompt('sync_custom', CHAT_PROMPT_JSON); const { promptData, promptSet, promptUpdate } = useChatPrompt('sync_custom', CHAT_PROMPT_JSON);
const { promptCacheCmd, promptCacheSet } = useCachePrompt(); const { promptCacheCmd, promptCacheSet } = useCachePrompt();
const { opData, opInit, opAdd, opRemove, opReplace, opSafeKey } = useData([]); const { opData, opInit, opAdd, opRemove, opReplace, opSafeKey } = useData([]);
const { columns, ...opInfo } = useColumns(syncColumns()); const { columns, ...opInfo } = useColumns(syncColumns());
@ -31,6 +33,11 @@ export default function SyncCustom() {
opInfo.resetRecord(); opInfo.resetRecord();
}; };
useInit(async () => {
const filePath = await path.join(await chatRoot(), 'chatgpt.log');
setLogPath(filePath);
});
useEffect(() => { useEffect(() => {
if (promptData.length <= 0) return; if (promptData.length <= 0) return;
opInit(promptData); opInit(promptData);
@ -38,24 +45,19 @@ export default function SyncCustom() {
useEffect(() => { useEffect(() => {
if (!opInfo.opType) return; if (!opInfo.opType) return;
if (opInfo.opType === 'sync') { (async () => {
const filename = `${opInfo?.opRecord?.id}.json`; if (opInfo.opType === 'sync') {
handleSync(filename).then((isOk: boolean) => { handleSync();
}
if (opInfo.opType === 'rowedit') {
await promptUpdate(opInfo?.opRecord?.id, 'name', opInfo?.opRecord?.name);
message.success('Name has been changed');
opInfo.resetRecord(); opInfo.resetRecord();
if (!isOk) return; }
const data = opReplace(opInfo?.opRecord?.[opSafeKey], { if (['edit', 'new'].includes(opInfo.opType)) {
...opInfo?.opRecord, setVisible(true);
last_updated: Date.now(), }
}); if (['delete'].includes(opInfo.opType)) {
promptSet(data);
opInfo.resetRecord();
});
}
if (['edit', 'new'].includes(opInfo.opType)) {
setVisible(true);
}
if (['delete'].includes(opInfo.opType)) {
(async () => {
try { try {
const file = await path.join( const file = await path.join(
await chatRoot(), await chatRoot(),
@ -68,63 +70,117 @@ export default function SyncCustom() {
promptSet(data); promptSet(data);
opInfo.resetRecord(); opInfo.resetRecord();
promptCacheCmd(); promptCacheCmd();
})(); }
} })();
}, [opInfo.opType, formRef]); }, [opInfo.opType, formRef]);
const handleSync = async (filename: string) => { const handleSync = async () => {
const filename = `${opInfo?.opRecord?.id}.json`;
const record = opInfo?.opRecord; const record = opInfo?.opRecord;
const isJson = /json$/.test(record?.ext);
const file = await path.join(await chatRoot(), 'cache_prompts', filename);
const filePath = await getPath(record);
// https or http // https or http
if (/^http/.test(record?.protocol)) { if (/^http/.test(record?.protocol)) {
const data = await invoke('sync_user_prompts', { url: filePath, dataType: record?.ext }); const isJson = /json$/.test(record?.url);
const file = await path.join(await chatRoot(), 'cache_prompts', filename);
const filePath = await getPath(record);
const data = await invoke('sync_user_prompts', {
url: filePath,
dataType: isJson ? 'json' : 'csv',
});
if (data) { if (data) {
await promptCacheSet(data as [], file); await promptCacheSet(data as [], file);
await promptCacheCmd(); await promptCacheCmd();
message.success('ChatGPT Prompts data has been synchronized!'); message.success('Prompts successfully synchronized');
return true; const data2 = opReplace(opInfo?.opRecord?.[opSafeKey], {
...opInfo?.opRecord,
last_updated: Date.now(),
});
promptSet(data2);
opInfo.resetRecord();
} else { } else {
message.error('ChatGPT Prompts data sync failed, please try again!'); message.error(
return false; 'Prompts synchronization failed, please try again (click to "View Log" for more details)',
);
} }
} }
// local opInfo.resetRecord();
if (isJson) { };
// parse json
const data = await readJSON(filePath, { isRoot: true }); const parseLocal = async (file: File): Promise<[boolean, any[] | null]> => {
await promptCacheSet(fmtData(data), file); if (file) {
} else { const fileData = await readFile(file);
// parse csv const isJSON = /json$/.test(file.name);
const data = await fs.readTextFile(filePath);
const list: Record<string, string>[] = await invoke('parse_prompt', { data }); if (isJSON) {
await promptCacheSet(fmtData(list), file); // parse json
try {
const jsonData = JSON.parse(fileData);
return [true, jsonData];
} catch (e) {
message.error('JSON parse error, please check your file');
return [false, null];
}
} else {
// parse csv
const list: Record<string, string>[] | null = await invoke('parse_prompt', {
data: fileData,
});
if (!list) {
message.error('CSV parse error, please check your file');
return [false, null];
} else {
return [true, list];
}
}
} }
await promptCacheCmd();
return true; message.error('File parsing exception');
return [false, null];
}; };
const handleOk = () => { const handleOk = () => {
formRef.current?.form?.validateFields().then(async (vals: Record<string, any>) => { formRef.current?.form?.validateFields().then(async (vals: Record<string, any>) => {
const file = await readFile(vals?.file?.file?.originFileObj);
vals.file = file;
if (opInfo.opType === 'new') { if (opInfo.opType === 'new') {
const data = opAdd(vals); if (vals.protocol !== 'local') {
promptSet(data); // http or https
message.success('Data added successfully'); delete vals.file;
const data = opAdd(vals);
await promptSet(data);
hide();
opInfo.setRecord(data[0], 'sync');
message.success('Data added successfully');
} else {
const file = vals?.file?.file?.originFileObj;
const data = opAdd(vals);
const parseData = await parseLocal(file);
if (parseData[0]) {
const id = data[0].id;
const filePath = await path.join(await chatRoot(), 'cache_prompts', `${id}.json`);
data[0].last_updated = Date.now();
await promptSet(data);
await promptCacheSet(fmtData(parseData[1] as []), filePath);
await promptCacheCmd();
hide();
message.success('Data added successfully');
}
}
} }
if (opInfo.opType === 'edit') { if (opInfo.opType === 'edit') {
delete vals.file;
const data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); const data = opReplace(opInfo?.opRecord?.[opSafeKey], vals);
promptSet(data); promptSet(data);
hide();
message.success('Data updated successfully'); message.success('Data updated successfully');
} }
hide();
}); });
}; };
const handleLog = () => {
shell.open(logPath);
};
return ( return (
<div> <div>
<Button <Button
@ -135,9 +191,12 @@ export default function SyncCustom() {
> >
Add Prompt Add Prompt
</Button> </Button>
<Button style={{ marginBottom: 10 }} onClick={handleLog}>
View Log
</Button>
<Table <Table
key="id" key="id"
rowKey="name" rowKey="id"
columns={columns} columns={columns}
scroll={{ x: 800 }} scroll={{ x: 800 }}
dataSource={opData} dataSource={opData}
@ -151,13 +210,13 @@ export default function SyncCustom() {
destroyOnClose destroyOnClose
maskClosable={false} maskClosable={false}
> >
<SyncForm ref={formRef} record={opInfo?.opRecord} type={opInfo.opType} /> <SyncForm ref={formRef} record={opInfo?.opRecord} />
</Modal> </Modal>
</div> </div>
); );
} }
function readFile(file: File) { function readFile(file: File): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (e: any) => resolve(e.target.result); reader.onload = (e: any) => resolve(e.target.result);

View File

@ -28,7 +28,11 @@ export default function SyncRecord() {
const selectedItems = rowSelection.selectedRowKeys || []; const selectedItems = rowSelection.selectedRowKeys || [];
useInit(async () => { useInit(async () => {
setFilePath(await getPath(state)); if (state.protocol === 'local') {
setFilePath('');
} else {
setFilePath(await getPath(state));
}
setJsonPath(await path.join(await chatRoot(), 'cache_prompts', `${state?.id}.json`)); setJsonPath(await path.join(await chatRoot(), 'cache_prompts', `${state?.id}.json`));
}); });