diff --git a/package.json b/package.json index dd9e962..6f8ca26 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src-tauri/src/app/cmd.rs b/src-tauri/src/app/cmd.rs index 6369ac1..b14aebd 100644 --- a/src-tauri/src/app/cmd.rs +++ b/src-tauri/src/app/cmd.rs @@ -203,14 +203,14 @@ pub fn get_download_list(pathname: &str) -> (Vec, PathBuf) { } #[command] -pub fn download_list(pathname: &str, filename: Option, id: Option) { +pub fn download_list(pathname: &str, dir: &str, filename: Option, id: Option) { 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("download")) + for entry in WalkDir::new(utils::chat_root().join(dir)) .into_iter() .filter_entry(|e| !utils::is_hidden(e)) .filter_map(|e| e.ok()) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8c84394..b41eee9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -30,7 +30,8 @@ async fn main() { trace: Color::Cyan, }; - cmd::download_list("chat.download.json", None, None); + 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(); diff --git a/src-tauri/src/scripts/export.js b/src-tauri/src/scripts/export.js index d5af93b..a5539d5 100644 --- a/src-tauri/src/scripts/export.js +++ b/src-tauri/src/scripts/export.js @@ -134,7 +134,9 @@ function addActionsButtons(actionsArea, TryAgainButton) { async function exportMarkdown() { const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); - await invoke('save_file', { name: `notes/${uid().toString(36)}.md`, content: data }); + 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 } = {}) { @@ -168,7 +170,7 @@ async function handleImg(imgData) { } const { pathname, id, filename } = getName(); await invoke('download', { name: `download/img/${id}.png`, blob: data }); - await invoke('download_list', { pathname, filename, id }); + await invoke('download_list', { pathname, filename, id, dir: 'download' }); } async function handlePdf(imgData, canvas, pixelRatio) { @@ -184,7 +186,7 @@ async function handlePdf(imgData, canvas, pixelRatio) { const { pathname, id, filename } = getName(); const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) }); - await invoke('download_list', { pathname, filename, id }); + await invoke('download_list', { pathname, filename, id, dir: 'download' }); } function getName() { diff --git a/src/hooks/useJson.ts b/src/hooks/useJson.ts index 00a5bd8..0bfee8b 100644 --- a/src/hooks/useJson.ts +++ b/src/hooks/useJson.ts @@ -9,6 +9,7 @@ export default function useJson(file: string) { const refreshJson = async () => { const data = await readJSON(file); setData(data); + return data; }; const updateJson = async (data: any) => { diff --git a/src/routes.tsx b/src/routes.tsx index b914935..41527cb 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,11 +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'; @@ -15,6 +16,7 @@ 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; @@ -35,15 +37,15 @@ export const routes: Array = [ element: , meta: { label: 'General', - icon: , + icon: , }, }, { - path: 'download', - element: , + path: '/notes', + element: , meta: { - label: 'Download', - icon: , + label: 'Notes', + icon: , }, }, { @@ -85,6 +87,14 @@ export const routes: Array = [ }, ], }, + { + path: 'download', + element: , + meta: { + label: 'Download', + icon: , + }, + }, ]; type MenuItem = Required['items'][number]; diff --git a/src/utils.ts b/src/utils.ts index 09a0648..b90a7bd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,7 @@ 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 = { diff --git a/src/view/download/index.tsx b/src/view/download/index.tsx index ae3909e..5156b78 100644 --- a/src/view/download/index.tsx +++ b/src/view/download/index.tsx @@ -18,7 +18,7 @@ function renderFile(buff: Uint8Array, type: string) { return URL.createObjectURL(new Blob([buff], { type: renderType })); } -export default function SyncPrompts() { +export default function Download() { const [downloadPath, setDownloadPath] = useState(''); const [source, setSource] = useState(''); const [isVisible, setVisible] = useState(false); @@ -51,9 +51,6 @@ export default function SyncPrompts() { setVisible(true); return; } - if (opInfo.opType === 'file') { - await shell.open(file); - } if (opInfo.opType === 'delete') { await fs.removeFile(file); await handleRefresh(); @@ -90,8 +87,9 @@ export default function SyncPrompts() { }; const handleRefresh = async () => { - await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON }); - refreshJson(); + await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON, dir: 'download' }); + const data = await refreshJson(); + opInit(data); }; const handleCancel = () => { @@ -107,7 +105,7 @@ export default function SyncPrompts() { <> [ + { + title: 'Name', + dataIndex: 'name', + fixed: 'left', + key: 'name', + width: 240, + render: (_: string, row: any, actions: any) => ( + + ), + }, + { + title: 'Path', + dataIndex: 'path', + key: 'path', + width: 200, + render: (_: string, row: any) => , + }, + { + title: 'Created', + dataIndex: 'created', + key: 'created', + width: 150, + render: fmtDate, + }, + { + title: 'Action', + fixed: 'right', + width: 160, + render: (_: any, row: any, actions: any) => { + return ( + + actions.setRecord(row, 'preview')}>Preview + 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) => { + const isImg = ['png'].includes(row?.ext); + return await path.join(await chatRoot(), 'notes', row.id) + `.${row.ext}`; +} diff --git a/src/view/notes/index.tsx b/src/view/notes/index.tsx new file mode 100644 index 0000000..b93bd35 --- /dev/null +++ b/src/view/notes/index.tsx @@ -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(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 ( +
+
+
+ {selectedItems.length > 0 && ( + <> + + + + Selected {selectedItems.length} items + + )} +
+
+ + + {opInfo?.opRecord?.name || ''}} + onCancel={handleCancel} + footer={false} + destroyOnClose + > + + ) : ( + + {children} + + ) + } + }} + /> + + + ) +} \ No newline at end of file