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

chore: export

This commit is contained in:
lencx 2023-01-15 01:18:03 +08:00
parent ae2c56805c
commit f1a807ed46
10 changed files with 264 additions and 19 deletions

View File

@ -40,7 +40,9 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^8.0.4",
"react-router-dom": "^6.4.5", "react-router-dom": "^6.4.5",
"react-syntax-highlighter": "^15.5.0",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"devDependencies": { "devDependencies": {
@ -50,6 +52,7 @@
"@types/node": "^18.7.10", "@types/node": "^18.7.10",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/uuid": "^9.0.0", "@types/uuid": "^9.0.0",
"@vitejs/plugin-react": "^3.0.0", "@vitejs/plugin-react": "^3.0.0",
"sass": "^1.56.2", "sass": "^1.56.2",

View File

@ -203,14 +203,14 @@ pub fn get_download_list(pathname: &str) -> (Vec<serde_json::Value>, PathBuf) {
} }
#[command] #[command]
pub fn download_list(pathname: &str, filename: Option<String>, id: Option<String>) { pub fn download_list(pathname: &str, dir: &str, filename: Option<String>, id: Option<String>) {
info!("download_list: {}", pathname); info!("download_list: {}", pathname);
let data = get_download_list(pathname); let data = get_download_list(pathname);
let mut list = vec![]; let mut list = vec![];
let mut idmap = HashMap::new(); let mut idmap = HashMap::new();
utils::vec_to_hashmap(data.0.into_iter(), "id", &mut idmap); 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() .into_iter()
.filter_entry(|e| !utils::is_hidden(e)) .filter_entry(|e| !utils::is_hidden(e))
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())

View File

@ -30,7 +30,8 @@ async fn main() {
trace: Color::Cyan, 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(); let chat_conf = ChatConfJson::get_chat_conf();

View File

@ -134,7 +134,9 @@ function addActionsButtons(actionsArea, TryAgainButton) {
async function exportMarkdown() { async function exportMarkdown() {
const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML); 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 } = {}) { function downloadThread({ as = Format.PNG } = {}) {
@ -168,7 +170,7 @@ async function handleImg(imgData) {
} }
const { pathname, id, filename } = getName(); const { pathname, id, filename } = getName();
await invoke('download', { name: `download/img/${id}.png`, blob: data }); 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) { async function handlePdf(imgData, canvas, pixelRatio) {
@ -184,7 +186,7 @@ async function handlePdf(imgData, canvas, pixelRatio) {
const { pathname, id, filename } = getName(); const { pathname, id, filename } = getName();
const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument()); 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', { 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() { function getName() {

View File

@ -9,6 +9,7 @@ export default function useJson<T>(file: string) {
const refreshJson = async () => { const refreshJson = async () => {
const data = await readJSON(file); const data = await readJSON(file);
setData(data); setData(data);
return data;
}; };
const updateJson = async (data: any) => { const updateJson = async (data: any) => {

22
src/routes.tsx vendored
View File

@ -1,11 +1,12 @@
import { useRoutes } from 'react-router-dom'; import { useRoutes } from 'react-router-dom';
import { import {
DesktopOutlined, SettingOutlined,
BulbOutlined, BulbOutlined,
SyncOutlined, SyncOutlined,
FileSyncOutlined, FileSyncOutlined,
UserOutlined, UserOutlined,
DownloadOutlined, DownloadOutlined,
FormOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
@ -15,6 +16,7 @@ import SyncPrompts from '@/view/model/SyncPrompts';
import SyncCustom from '@/view/model/SyncCustom'; import SyncCustom from '@/view/model/SyncCustom';
import SyncRecord from '@/view/model/SyncRecord'; import SyncRecord from '@/view/model/SyncRecord';
import Download from '@/view/download'; import Download from '@/view/download';
import Notes from '@/view/notes';
export type ChatRouteMetaObject = { export type ChatRouteMetaObject = {
label: string; label: string;
@ -35,15 +37,15 @@ export const routes: Array<ChatRouteObject> = [
element: <General />, element: <General />,
meta: { meta: {
label: 'General', label: 'General',
icon: <DesktopOutlined />, icon: <SettingOutlined />,
}, },
}, },
{ {
path: 'download', path: '/notes',
element: <Download />, element: <Notes />,
meta: { meta: {
label: 'Download', label: 'Notes',
icon: <DownloadOutlined />, icon: <FormOutlined />,
}, },
}, },
{ {
@ -85,6 +87,14 @@ export const routes: Array<ChatRouteObject> = [
}, },
], ],
}, },
{
path: 'download',
element: <Download />,
meta: {
label: 'Download',
icon: <DownloadOutlined />,
},
},
]; ];
type MenuItem = Required<MenuProps>['items'][number]; type MenuItem = Required<MenuProps>['items'][number];

1
src/utils.ts vendored
View File

@ -5,6 +5,7 @@ import dayjs from 'dayjs';
export const CHAT_MODEL_JSON = 'chat.model.json'; export const CHAT_MODEL_JSON = 'chat.model.json';
export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json'; export const CHAT_MODEL_CMD_JSON = 'chat.model.cmd.json';
export const CHAT_DOWNLOAD_JSON = 'chat.download.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 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 GITHUB_PROMPTS_CSV_URL = 'https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv';
export const DISABLE_AUTO_COMPLETE = { export const DISABLE_AUTO_COMPLETE = {

View File

@ -18,7 +18,7 @@ function renderFile(buff: Uint8Array, type: string) {
return URL.createObjectURL(new Blob([buff], { type: renderType })); return URL.createObjectURL(new Blob([buff], { type: renderType }));
} }
export default function SyncPrompts() { export default function Download() {
const [downloadPath, setDownloadPath] = useState(''); const [downloadPath, setDownloadPath] = useState('');
const [source, setSource] = useState(''); const [source, setSource] = useState('');
const [isVisible, setVisible] = useState(false); const [isVisible, setVisible] = useState(false);
@ -51,9 +51,6 @@ export default function SyncPrompts() {
setVisible(true); setVisible(true);
return; return;
} }
if (opInfo.opType === 'file') {
await shell.open(file);
}
if (opInfo.opType === 'delete') { if (opInfo.opType === 'delete') {
await fs.removeFile(file); await fs.removeFile(file);
await handleRefresh(); await handleRefresh();
@ -90,8 +87,9 @@ export default function SyncPrompts() {
}; };
const handleRefresh = async () => { const handleRefresh = async () => {
await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON }); await invoke('download_list', { pathname: CHAT_DOWNLOAD_JSON, dir: 'download' });
refreshJson(); const data = await refreshJson();
opInit(data);
}; };
const handleCancel = () => { const handleCancel = () => {
@ -107,7 +105,7 @@ export default function SyncPrompts() {
<> <>
<Popconfirm <Popconfirm
overlayStyle={{ width: 250 }} overlayStyle={{ width: 250 }}
title="Sync will overwrite the previous data, confirm to sync?" title="Files cannot be recovered after deletion, are you sure you want to delete them?"
placement="topLeft" placement="topLeft"
onConfirm={handleDelete} onConfirm={handleDelete}
okText="Yes" okText="Yes"

69
src/view/notes/config.tsx vendored Normal file
View 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
View 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>
)
}