From 8319eae519491ca02e6548d54c3e920bd31ef269 Mon Sep 17 00:00:00 2001 From: lencx Date: Fri, 23 Dec 2022 00:43:58 +0800 Subject: [PATCH] chore: sync --- UPDATE_LOG.md | 5 + src-tauri/src/app/cmd.rs | 1 + src/hooks/useChatModel.ts | 8 +- src/utils.ts | 28 ++++-- src/view/model/SyncMore/Form.tsx | 105 +++++++++++++++++++++ src/view/model/SyncMore/config.tsx | 72 ++++++++++++-- src/view/model/SyncMore/index.tsx | 136 +++++++++++++++++++++++++-- src/view/model/SyncPrompts/index.tsx | 4 +- src/view/model/UserCustom/Form.tsx | 6 +- src/view/model/UserCustom/index.tsx | 15 +-- 10 files changed, 345 insertions(+), 35 deletions(-) create mode 100644 src/view/model/SyncMore/Form.tsx diff --git a/UPDATE_LOG.md b/UPDATE_LOG.md index 409c78c..63fe27a 100644 --- a/UPDATE_LOG.md +++ b/UPDATE_LOG.md @@ -2,8 +2,13 @@ ## v0.5.2 +fix: +- windows show Chinese when upgrading + feat: - optimize the generated pdf file size +- menu added `Sync Prompts` +- the slash command is triggered by the enter key ## v0.5.1 diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index fc5d3c0..6ae243b 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -74,6 +74,7 @@ pub fn get_chat_model() -> serde_json::Value { #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct PromptRecord { + pub cmd: Option, pub act: String, pub prompt: String, } diff --git a/src/hooks/useChatModel.ts b/src/hooks/useChatModel.ts index d9a6b55..c7cdbe5 100644 --- a/src/hooks/useChatModel.ts +++ b/src/hooks/useChatModel.ts @@ -5,18 +5,20 @@ import { invoke } from '@tauri-apps/api'; import { CHAT_MODEL_JSON, readJSON, writeJSON } from '@/utils'; import useInit from '@/hooks/useInit'; -export default function useChatModel(key: string) { +export default function useChatModel(key: string, file = CHAT_MODEL_JSON) { const [modelJson, setModelJson] = useState>({}); useInit(async () => { - const data = await readJSON(CHAT_MODEL_JSON, { name: 'ChatGPT Model', [key]: [] }); + const data = await readJSON(file, { + defaultVal: { name: 'ChatGPT Model', [key]: [] }, + }); setModelJson(data); }); const modelSet = async (data: Record[]) => { const oData = clone(modelJson); oData[key] = data; - await writeJSON(CHAT_MODEL_JSON, oData); + await writeJSON(file, oData); await invoke('window_reload', { label: 'core' }); setModelJson(oData); } diff --git a/src/utils.ts b/src/utils.ts index 5f7909a..ff92424 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,9 @@ -import { readTextFile, writeTextFile, exists } from '@tauri-apps/api/fs'; -import { homeDir, join } from '@tauri-apps/api/path'; +import { readTextFile, writeTextFile, exists, createDir, BaseDirectory } from '@tauri-apps/api/fs'; +import { homeDir, join, dirname } from '@tauri-apps/api/path'; import dayjs from 'dayjs'; export const CHAT_MODEL_JSON = 'chat.model.json'; +export const CHAT_MODEL_SYNC_JSON = 'chat.model.sync.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 = { @@ -19,18 +20,24 @@ export const chatModelPath = async (): Promise => { return join(await chatRoot(), CHAT_MODEL_JSON); } +export const chatModelSyncPath = async (): Promise => { + return join(await chatRoot(), CHAT_MODEL_SYNC_JSON); +} + export const chatPromptsPath = async (): Promise => { return join(await chatRoot(), CHAT_PROMPTS_CSV); } -export const readJSON = async (path: string, defaultVal = {}) => { +type readJSONOpts = { defaultVal?: Record, isRoot?: boolean }; +export const readJSON = async (path: string, opts: readJSONOpts = {}) => { + const { defaultVal = {}, isRoot = false } = opts; const root = await chatRoot(); - const file = await join(root, path); + const file = await join(isRoot ? '' : root, path); if (!await exists(file)) { writeTextFile(file, JSON.stringify({ name: 'ChatGPT', - link: 'https://github.com/lencx/ChatGPT/blob/main/chat.model.md', + link: 'https://github.com/lencx/ChatGPT', ...defaultVal, }, null, 2)) } @@ -42,9 +49,16 @@ export const readJSON = async (path: string, defaultVal = {}) => { } } -export const writeJSON = async (path: string, data: Record) => { +type writeJSONOpts = { dir?: string, isRoot?: boolean }; +export const writeJSON = async (path: string, data: Record, opts: writeJSONOpts = {}) => { + const { isRoot = false, dir = '' } = opts; const root = await chatRoot(); - const file = await join(root, path); + const file = await join(isRoot ? '' : root, path); + + if (isRoot && !await exists(await dirname(file))) { + await createDir(await join('.chatgpt', dir), { dir: BaseDirectory.Home }); + } + await writeTextFile(file, JSON.stringify(data, null, 2)); } diff --git a/src/view/model/SyncMore/Form.tsx b/src/view/model/SyncMore/Form.tsx new file mode 100644 index 0000000..f823808 --- /dev/null +++ b/src/view/model/SyncMore/Form.tsx @@ -0,0 +1,105 @@ +import { useEffect, useState, ForwardRefRenderFunction, useImperativeHandle, forwardRef } from 'react'; +import { Form, Input, Select, Tooltip } from 'antd'; +import { v4 } from 'uuid'; +import type { FormProps } from 'antd'; + +import { DISABLE_AUTO_COMPLETE, chatRoot } from '@/utils'; +import useInit from '@/hooks/useInit'; + +interface SyncFormProps { + record?: Record | null; +} + +const initFormValue = { + act: '', + enable: true, + tags: [], + prompt: '', +}; + +const SyncForm: ForwardRefRenderFunction = ({ record }, ref) => { + const [form] = Form.useForm(); + useImperativeHandle(ref, () => ({ form })); + const [root, setRoot] = useState(''); + + useInit(async () => { + setRoot(await chatRoot()); + }); + + useEffect(() => { + if (record) { + form.setFieldsValue(record); + } + }, [record]); + + const pathOptions = ( + + + + ); + const extOptions = ( + + + + ); + + const jsonTip = ( + {JSON.stringify([ + { cmd: '', act: '', prompt: '' }, + { cmd: '', act: '', prompt: '' }, + ], null, 2)}} + > + JSON + + ); + + const csvTip = ( + {`"cmd","act","prompt" +"cmd","act","prompt" +"cmd","act","prompt" +"cmd","act","prompt"`}} + > + CSV + + ); + + return ( + <> +
+ + + + + + + +
+
+

The file supports only {csvTip} and {jsonTip} formats.

+
+ + ) +} + +export default forwardRef(SyncForm); diff --git a/src/view/model/SyncMore/config.tsx b/src/view/model/SyncMore/config.tsx index 3fc3429..345ceff 100644 --- a/src/view/model/SyncMore/config.tsx +++ b/src/view/model/SyncMore/config.tsx @@ -1,15 +1,73 @@ -export const recordColumns = () => [ +import { useState } from 'react'; +import { Tag, Space, Popconfirm } from 'antd'; +import { shell, path } from '@tauri-apps/api'; + +import useInit from '@/hooks/useInit'; +import { chatRoot, fmtDate } from '@/utils'; + +export const pathColumns = () => [ { - title: 'URL', - dataIndex: 'url', - key: 'url', + title: 'Name', + dataIndex: 'name', + key: 'name', + width: 100, }, { - title: 'File Type', - dataIndex: 'file_type', - key: 'file_type', + title: 'Protocol', + dataIndex: 'protocol', + key: 'protocol', + width: 80, + render: (v: string) => {v}, + }, + { + title: 'PATH', + dataIndex: 'path', + key: 'path', + width: 180, + render: (_: string, row: any) => + }, + { + title: 'Last updated', + dataIndex: 'last_updated', + key: 'last_updated', + width: 140, + render: fmtDate, }, { title: 'Action', + fixed: 'right', + width: 140, + render: (_: any, row: any, actions: any) => { + return ( + + actions.setRecord(row, 'sync')}>Sync + actions.setRecord(row, 'edit')}>Edit + actions.setRecord(row, 'delete')} + okText="Yes" + cancelText="No" + > + Delete + + + ) + } } ]; + +const RenderPath = ({ row }: any) => { + const [filePath, setFilePath] = useState(''); + useInit(async () => { + setFilePath(await getPath(row)); + }) + return shell.open(filePath)}>{filePath} +}; + +export const getPath = async (row: any) => { + if (!/^http/.test(row.protocol)) { + return await path.join(await chatRoot(), row.path) + `.${row.ext}`; + } else { + return `${row.protocol}://${row.path}.${row.ext}`; + } +} \ No newline at end of file diff --git a/src/view/model/SyncMore/index.tsx b/src/view/model/SyncMore/index.tsx index a88e96b..ad9d63c 100644 --- a/src/view/model/SyncMore/index.tsx +++ b/src/view/model/SyncMore/index.tsx @@ -1,20 +1,144 @@ -import { Table, Button } from 'antd'; +import { useState, useRef, useEffect } from 'react'; +import { Table, Modal, Button, message } from 'antd'; +import { invoke, http, path, fs } from '@tauri-apps/api'; +import useData from '@/hooks/useData'; +import useChatModel from '@/hooks/useChatModel'; +import useColumns from '@/hooks/useColumns'; import { TABLE_PAGINATION } from '@/hooks/useTable'; +import { CHAT_MODEL_SYNC_JSON, chatRoot, writeJSON, readJSON, genCmd } from '@/utils'; +import { pathColumns, getPath } from './config'; +import SyncForm from './Form'; import './index.scss'; +const setTag = (data: Record[]) => data.map((i) => ({ ...i, tags: ['user-sync'], enable: true })) + export default function SyncMore() { + const [isVisible, setVisible] = useState(false); + // const [modelPath, setChatModelPath] = useState(''); + const { modelData, modelSet } = useChatModel('sync_url', CHAT_MODEL_SYNC_JSON); + const { opData, opInit, opAdd, opRemove, opReplace, opSafeKey } = useData([]); + const { columns, ...opInfo } = useColumns(pathColumns()); + const formRef = useRef(null); + + const hide = () => { + setVisible(false); + opInfo.resetRecord(); + }; + + useEffect(() => { + if (modelData.length <= 0) return; + opInit(modelData); + }, [modelData]); + + useEffect(() => { + if (!opInfo.opType) return; + if (opInfo.opType === 'sync') { + const filename = `${opInfo?.opRecord?.id}.json`; + handleSync(filename).then(() => { + const data = opReplace(opInfo?.opRecord?.[opSafeKey], { ...opInfo?.opRecord, last_updated: Date.now() }); + console.log('«38» /model/SyncMore/index.tsx ~> ', data); + + modelSet(data); + opInfo.resetRecord(); + }); + } + if (['edit', 'new'].includes(opInfo.opType)) { + setVisible(true); + } + if (['delete'].includes(opInfo.opType)) { + const data = opRemove(opInfo?.opRecord?.[opSafeKey]); + modelSet(data); + opInfo.resetRecord(); + } + }, [opInfo.opType, formRef]); + + const handleSync = async (filename: string) => { + const record = opInfo?.opRecord; + const isJson = /json$/.test(record?.ext); + const file = await path.join(await chatRoot(), 'cache_sync', filename); + const filePath = await getPath(record); + + // https or http + if (/^http/.test(record?.protocol)) { + const res = await http.fetch(filePath, { + method: 'GET', + responseType: isJson ? 1 : 2, + }); + if (res.ok) { + if (isJson) { + // parse json + writeJSON(file, setTag(Array.isArray(res?.data) ? res?.data : []), { isRoot: true, dir: 'cache_sync' }); + } else { + // parse csv + const list: Record[] = await invoke('parse_prompt', { data: res?.data }); + const fmtList = list.map(i => ({ ...i, cmd: i.cmd ? i.cmd : genCmd(i.act), enable: true, tags: ['user-sync'] })); + await writeJSON(file, fmtList, { isRoot: true, dir: 'cache_sync' }); + } + message.success('ChatGPT Prompts data has been synchronized!'); + } else { + message.error('ChatGPT Prompts data sync failed, please try again!'); + } + return; + } + // local + if (isJson) { + // parse json + const data = await readJSON(filePath, { isRoot: true }); + await writeJSON(file, setTag(Array.isArray(data) ? data : []), { isRoot: true, dir: 'cache_sync' }); + } else { + // parse csv + const data = await fs.readTextFile(filePath); + const list: Record[] = await invoke('parse_prompt', { data }); + const fmtList = list.map(i => ({ ...i, cmd: i.cmd ? i.cmd : genCmd(i.act), enable: true, tags: ['user-sync'] })); + await writeJSON(file, fmtList, { isRoot: true, dir: 'cache_sync' }); + } + }; + + const handleOk = () => { + formRef.current?.form?.validateFields() + .then((vals: Record) => { + let data = []; + switch (opInfo.opType) { + case 'new': data = opAdd(vals); break; + case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break; + default: break; + } + console.log('«95» /model/SyncMore/index.tsx ~> ', data); + + modelSet(data); + opInfo.setExtra(Date.now()); + hide(); + }) + }; + return (
- + + + + ) } \ No newline at end of file diff --git a/src/view/model/SyncPrompts/index.tsx b/src/view/model/SyncPrompts/index.tsx index 70136df..d6cabd6 100644 --- a/src/view/model/SyncPrompts/index.tsx +++ b/src/view/model/SyncPrompts/index.tsx @@ -44,7 +44,7 @@ export default function LanguageModel() { await writeTextFile(await chatPromptsPath(), data); const list: Record[] = await invoke('parse_prompt', { data }); opInit(list); - modelSet(list.map(i => ({ cmd: genCmd(i.act), enable: true, tags: ['chatgpt-prompts'], ...i }))); + modelSet(list.map(i => ({ ...i, cmd: i.cmd ? i.cmd : genCmd(i.act), enable: true, tags: ['chatgpt-prompts'] }))); setLastUpdated(fmtDate(Date.now()) as any); message.success('ChatGPT Prompts data has been synchronized!'); } else { @@ -88,7 +88,7 @@ export default function LanguageModel() {
URL: f/awesome-chatgpt-prompts/prompts.csv - {lastUpdated && Last updated on {fmtDate(lastUpdated)}} + {lastUpdated && Last updated on {fmtDate(lastUpdated)}}
| null; } @@ -16,7 +16,7 @@ const initFormValue = { prompt: '', }; -const LanguageModel: ForwardRefRenderFunction = ({ record }, ref) => { +const UserCustomForm: ForwardRefRenderFunction = ({ record }, ref) => { const [form] = Form.useForm(); useImperativeHandle(ref, () => ({ form })); @@ -63,4 +63,4 @@ const LanguageModel: ForwardRefRenderFunction = ( ) } -export default forwardRef(LanguageModel); +export default forwardRef(UserCustomForm); diff --git a/src/view/model/UserCustom/index.tsx b/src/view/model/UserCustom/index.tsx index c9d23ab..019e9da 100644 --- a/src/view/model/UserCustom/index.tsx +++ b/src/view/model/UserCustom/index.tsx @@ -7,9 +7,9 @@ import useData from '@/hooks/useData'; import useChatModel from '@/hooks/useChatModel'; import useColumns from '@/hooks/useColumns'; import { TABLE_PAGINATION } from '@/hooks/useTable'; -import { chatModelPath, genCmd } from '@/utils'; +import { chatModelPath } from '@/utils'; import { modelColumns } from './config'; -import LanguageModelForm from './Form'; +import UserCustomForm from './Form'; import './index.scss'; export default function LanguageModel() { @@ -23,7 +23,7 @@ export default function LanguageModel() { useEffect(() => { if (modelData.length <= 0) return; opInit(modelData); - }, [modelData]) + }, [modelData]); useEffect(() => { if (!opInfo.opType) return; @@ -67,7 +67,8 @@ export default function LanguageModel() { case 'edit': data = opReplace(opInfo?.opRecord?.[opSafeKey], vals); break; default: break; } - modelSet(data) + modelSet(data); + opInfo.setExtra(Date.now()); hide(); }) }; @@ -76,14 +77,14 @@ export default function LanguageModel() { invoke('open_file', { path: modelPath }); }; - const modalTitle = `${({ new: 'Create', edit: 'Edit' })[opInfo.opType]} Language Model`; + const modalTitle = `${({ new: 'Create', edit: 'Edit' })[opInfo.opType]} Model`; return (
PATH: {modelPath}
- + )