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>
[![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)\
![visitor](https://visitor-badge.glitch.me/badge?page_id=lencx.chatgpt)
[![简体中文 badge](https://img.shields.io/badge/%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-Simplified%20Chinese-blue)](./README-ZH_CN.md)
[![ChatGPT downloads](https://img.shields.io/github/downloads/lencx/ChatGPT/total.svg?style=flat-square)](https://github.com/lencx/ChatGPT/releases)
[![chat](https://img.shields.io/badge/chat-discord-blue?style=flat&logo=discord)](https://discord.gg/aPhCRf4zZr)
[![lencx](https://img.shields.io/badge/follow-lencx__-blue?style=flat&logo=Twitter)](https://twitter.com/lencx_)
<!-- [![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>
**🛑 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:**

View File

@ -9,6 +9,8 @@ use std::{collections::HashMap, fs, path::PathBuf, vec};
use tauri::{api, command, AppHandle, Manager};
use walkdir::WalkDir;
use super::fs_extra::Error;
#[command]
pub fn get_chat_prompt_cmd() -> serde_json::Value {
let path = utils::app_root().join("chat.prompt.cmd.json");
@ -24,23 +26,28 @@ pub struct PromptBaseRecord {
}
#[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 list = vec![];
for result in rdr.deserialize() {
let record: PromptBaseRecord = result.unwrap_or_else(|err| {
error!("parse_prompt: {}", err);
PromptBaseRecord {
cmd: None,
act: "".to_string(),
prompt: "".to_string(),
}
});
for result in rdr.deserialize::<PromptBaseRecord>() {
match result {
Ok(record) => {
if !record.act.is_empty() {
list.push(record);
}
}
list
Err(err) => {
error!("parse_prompt: {}", err);
}
}
}
if list.is_empty() {
None
} else {
Some(list)
}
}
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
@ -165,11 +172,12 @@ pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<PromptRecord>
.unwrap();
if let Some(v) = res {
let data = parse_prompt(v)
if let Some(data) = parse_prompt(v) {
let transformed_data = data
.iter()
.map(move |i| PromptRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
.map(|i| PromptRecord {
cmd: if let Some(cmd) = &i.cmd {
cmd.clone()
} else {
utils::gen_cmd(i.act.clone())
},
@ -180,7 +188,7 @@ pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<PromptRecord>
})
.collect::<Vec<PromptRecord>>();
let data2 = data.clone();
let data2 = transformed_data;
let prompts = utils::app_root().join("chat.prompt.json");
let prompt_cmd = utils::app_root().join("chat.prompt.cmd.json");
@ -240,13 +248,14 @@ pub async fn sync_prompts(app: AppHandle, time: u64) -> Option<Vec<PromptRecord>
api::dialog::message(
app.get_window("core").as_ref(),
"Sync Prompts",
"ChatGPT Prompts data has been synchronized!",
"Prompts data has been synchronized!",
);
window::cmd::window_reload(app.clone(), "core");
window::cmd::window_reload(app, "tray");
return Some(data2);
}
}
None
}
@ -260,29 +269,31 @@ pub async fn sync_user_prompts(url: String, data_type: String) -> Option<Vec<Pro
});
if let Some(v) = res {
let data;
if data_type == "csv" {
let data: Option<Vec<PromptBaseRecord>> = if data_type == "csv" {
info!("chatgpt_http_csv_parse");
data = parse_prompt(v);
parse_prompt(v)
} else if data_type == "json" {
info!("chatgpt_http_json_parse");
data = serde_json::from_str(&v).unwrap_or_else(|err| {
match serde_json::from_str::<Vec<PromptBaseRecord>>(&v) {
Ok(parsed) => Some(parsed),
Err(err) => {
error!("chatgpt_http_json_parse: {}", err);
vec![]
});
None
}
}
} else {
error!("chatgpt_http_unknown_type");
data = vec![];
}
None
};
let data = data
if let Some(base_records) = data {
let data = base_records
.iter()
.map(move |i| PromptRecord {
cmd: if i.cmd.is_some() {
i.cmd.clone().unwrap()
} else {
utils::gen_cmd(i.act.clone())
},
.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()],
@ -292,6 +303,7 @@ pub async fn sync_user_prompts(url: String, data_type: String) -> Option<Vec<Pro
return Some(data);
}
}
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 oData = clone(promptJson);
oData[key] = data;
await writeJSON(file, 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 = '') {

View File

@ -5,17 +5,15 @@ import {
useImperativeHandle,
forwardRef,
} 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 { InboxOutlined } from '@ant-design/icons';
import { UploadOutlined } from '@ant-design/icons';
import type { FormProps, RadioChangeEvent, UploadProps, UploadFile } from 'antd';
import { DISABLE_AUTO_COMPLETE, chatRoot } from '@/utils';
// import useInit from '@/hooks/useInit';
interface SyncFormProps {
record?: Record<string | symbol, any> | null;
type: string;
}
const initFormValue = {
@ -25,43 +23,18 @@ const initFormValue = {
protocol: 'https',
};
const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record, type }, ref) => {
// const isDisabled = type === 'edit';
const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record }, ref) => {
const [form] = Form.useForm();
useImperativeHandle(ref, () => ({ form }));
// const [root, setRoot] = useState('');
const [protocol, setProtocol] = useState('https');
const [fileList, setFileList] = useState<UploadFile[]>([]);
// useInit(async () => {
// setRoot(await chatRoot());
// });
useEffect(() => {
if (record) {
form.setFieldsValue(record);
}
}, [record]);
// const pathOptions = (
// <Form.Item noStyle name="protocol" initialValue="https">
// <Select disabled={isDisabled}>
// <Select.Option value="local">{root}</Select.Option>
// <Select.Option value="http">http://</Select.Option>
// <Select.Option value="https">https://</Select.Option>
// </Select>
// </Form.Item>
// );
// const extOptions = (
// <Form.Item noStyle name="ext" initialValue="json">
// <Select disabled={isDisabled}>
// <Select.Option value="csv">.csv</Select.Option>
// <Select.Option value="json">.json</Select.Option>
// </Select>
// </Form.Item>
// );
const jsonTip = (
<Tooltip
title={
@ -135,12 +108,8 @@ const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record,
<Radio value="local">local</Radio>
</Radio.Group>
</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) && (
<div style={{ height: 180 }}>
<Form.Item
label="URL"
name="url"
@ -151,33 +120,38 @@ const SyncForm: ForwardRefRenderFunction<FormProps, SyncFormProps> = ({ record,
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}://`}
// addonAfter={extOptions}
{...DISABLE_AUTO_COMPLETE}
/>
</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' && (
<Form.Item
name="file"
label="File"
rules={[{ required: true, message: 'Please select a file!' }]}
style={{ height: 200 }}
style={{ height: 168 }}
>
<Upload.Dragger {...uploadOptions}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
<Button icon={<UploadOutlined />}>Click to Upload</Button>
<p className="ant-upload-hint">
Only {csvTip} or {jsonTip} file formats are supported.
</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>
</Form.Item>
)}

View File

@ -4,6 +4,7 @@ import { HistoryOutlined } from '@ant-design/icons';
import { shell, path } from '@tauri-apps/api';
import { Link } from 'react-router-dom';
import { EditRow } from '@/hooks/useColumns';
import useInit from '@/hooks/useInit';
import { chatRoot, fmtDate } from '@/utils';
@ -13,6 +14,9 @@ export const syncColumns = () => [
dataIndex: 'name',
key: 'name',
width: 100,
render: (_: string, row: any, actions: any) => (
<EditRow rowKey="name" row={row} actions={actions} />
),
},
{
title: 'Protocol',
@ -47,6 +51,7 @@ export const syncColumns = () => [
render: (_: any, row: any, actions: any) => {
return (
<Space>
{row.protocol !== 'local' && (
<Popconfirm
overlayStyle={{ width: 250 }}
title="Sync will overwrite the previous data, confirm to sync?"
@ -56,12 +61,12 @@ export const syncColumns = () => [
>
<a>Sync</a>
</Popconfirm>
)}
{row.last_updated && (
<Link to={`${row.id}`} state={row}>
View
</Link>
)}
<a onClick={() => actions.setRecord(row, 'edit')}>Edit</a>
<Popconfirm
title="Are you sure to delete this path?"
onConfirm={() => actions.setRecord(row, 'delete')}
@ -86,8 +91,8 @@ const RenderPath = ({ row }: any) => {
export const getPath = async (row: any) => {
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 {
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 { 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 useInit from '@/hooks/useInit';
import useColumns from '@/hooks/useColumns';
import { TABLE_PAGINATION } from '@/hooks/useTable';
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 SyncForm from './Form';
@ -19,8 +20,9 @@ const fmtData = (data: Record<string, any>[] = []) =>
}));
export default function SyncCustom() {
const [logPath, setLogPath] = useState('');
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 { opData, opInit, opAdd, opRemove, opReplace, opSafeKey } = useData([]);
const { columns, ...opInfo } = useColumns(syncColumns());
@ -31,6 +33,11 @@ export default function SyncCustom() {
opInfo.resetRecord();
};
useInit(async () => {
const filePath = await path.join(await chatRoot(), 'chatgpt.log');
setLogPath(filePath);
});
useEffect(() => {
if (promptData.length <= 0) return;
opInit(promptData);
@ -38,24 +45,19 @@ export default function SyncCustom() {
useEffect(() => {
if (!opInfo.opType) return;
(async () => {
if (opInfo.opType === 'sync') {
const filename = `${opInfo?.opRecord?.id}.json`;
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();
if (!isOk) return;
const data = opReplace(opInfo?.opRecord?.[opSafeKey], {
...opInfo?.opRecord,
last_updated: Date.now(),
});
promptSet(data);
opInfo.resetRecord();
});
}
if (['edit', 'new'].includes(opInfo.opType)) {
setVisible(true);
}
if (['delete'].includes(opInfo.opType)) {
(async () => {
try {
const file = await path.join(
await chatRoot(),
@ -68,63 +70,117 @@ export default function SyncCustom() {
promptSet(data);
opInfo.resetRecord();
promptCacheCmd();
})();
}
})();
}, [opInfo.opType, formRef]);
const handleSync = async (filename: string) => {
const handleSync = async () => {
const filename = `${opInfo?.opRecord?.id}.json`;
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
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) {
await promptCacheSet(data as [], file);
await promptCacheCmd();
message.success('ChatGPT Prompts data has been synchronized!');
return true;
message.success('Prompts successfully synchronized');
const data2 = opReplace(opInfo?.opRecord?.[opSafeKey], {
...opInfo?.opRecord,
last_updated: Date.now(),
});
promptSet(data2);
opInfo.resetRecord();
} else {
message.error('ChatGPT Prompts data sync failed, please try again!');
return false;
message.error(
'Prompts synchronization failed, please try again (click to "View Log" for more details)',
);
}
}
// local
if (isJson) {
opInfo.resetRecord();
};
const parseLocal = async (file: File): Promise<[boolean, any[] | null]> => {
if (file) {
const fileData = await readFile(file);
const isJSON = /json$/.test(file.name);
if (isJSON) {
// parse json
const data = await readJSON(filePath, { isRoot: true });
await promptCacheSet(fmtData(data), file);
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 data = await fs.readTextFile(filePath);
const list: Record<string, string>[] = await invoke('parse_prompt', { data });
await promptCacheSet(fmtData(list), file);
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 = () => {
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 (vals.protocol !== 'local') {
// http or https
delete vals.file;
const data = opAdd(vals);
promptSet(data);
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') {
delete vals.file;
const data = opReplace(opInfo?.opRecord?.[opSafeKey], vals);
promptSet(data);
hide();
message.success('Data updated successfully');
}
hide();
});
};
const handleLog = () => {
shell.open(logPath);
};
return (
<div>
<Button
@ -135,9 +191,12 @@ export default function SyncCustom() {
>
Add Prompt
</Button>
<Button style={{ marginBottom: 10 }} onClick={handleLog}>
View Log
</Button>
<Table
key="id"
rowKey="name"
rowKey="id"
columns={columns}
scroll={{ x: 800 }}
dataSource={opData}
@ -151,13 +210,13 @@ export default function SyncCustom() {
destroyOnClose
maskClosable={false}
>
<SyncForm ref={formRef} record={opInfo?.opRecord} type={opInfo.opType} />
<SyncForm ref={formRef} record={opInfo?.opRecord} />
</Modal>
</div>
);
}
function readFile(file: File) {
function readFile(file: File): Promise<string> {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = (e: any) => resolve(e.target.result);

View File

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