chore: export

This commit is contained in:
lencx 2023-01-14 23:31:12 +08:00
parent a2fcfa3b89
commit ae2c56805c
15 changed files with 264 additions and 69 deletions

View File

@ -207,8 +207,8 @@ pub fn download_list(pathname: &str, filename: Option<String>, id: Option<String
info!("download_list: {}", pathname);
let data = get_download_list(pathname);
let mut list = vec![];
let mut my_hashmap = HashMap::new();
utils::vec_to_hashmap(data.0.into_iter(), "id", &mut my_hashmap);
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"))
.into_iter()
@ -229,7 +229,19 @@ pub fn download_list(pathname: &str, filename: Option<String>, id: Option<String
ext: fext.to_string(),
created: fs_extra::system_time_to_ms(metadata.created()),
};
if my_hashmap.get(fid).is_some() && filename.is_some() && id.is_some() {
if idmap.get(fid).is_some() {
let name = idmap.get(fid).unwrap().get("name").unwrap().clone();
match name {
serde_json::Value::String(v) => {
file_data.name = v.clone();
v
}
_ => "".to_string(),
};
}
if filename.is_some() && id.is_some() {
if let Some(ref v) = id {
if fid == v {
if let Some(ref v2) = filename {

View File

@ -52,11 +52,13 @@ function shouldAddButtons(actionsArea) {
return !/download-/.test(button.id);
});
if (/Stop generating/ig.test(buttons[0].innerText)) {
const stopBtn = buttons?.[0]?.innerText;
if (/Stop generating/ig.test(stopBtn)) {
return false;
}
if (buttons.length === 2 && (/Regenerate response/ig.test(buttons[0].innerText) || buttons[1].innerText === '')) {
if (buttons.length === 2 && (/Regenerate response/ig.test(stopBtn) || buttons[1].innerText === '')) {
return true;
}
@ -132,7 +134,7 @@ function addActionsButtons(actionsArea, TryAgainButton) {
async function exportMarkdown() {
const data = ExportMD.turndown(document.querySelector("main div>div>div").innerHTML);
await invoke('save_file', { name: `notes/${Date.now().toString(36)}.md`, content: data });
await invoke('save_file', { name: `notes/${uid().toString(36)}.md`, content: data });
}
function downloadThread({ as = Format.PNG } = {}) {
@ -164,8 +166,9 @@ async function handleImg(imgData) {
for (let i = 0; i < binaryData.length; i++) {
data.push(binaryData.charCodeAt(i));
}
await invoke('download', { name: `download/img/${Date.now().toString(36)}.png`, blob: data });
await invoke('download_list');
const { pathname, id, filename } = getName();
await invoke('download', { name: `download/img/${id}.png`, blob: data });
await invoke('download_list', { pathname, filename, id });
}
async function handlePdf(imgData, canvas, pixelRatio) {
@ -178,10 +181,16 @@ async function handlePdf(imgData, canvas, pixelRatio) {
var pdfWidth = pdf.internal.pageSize.getWidth();
var pdfHeight = pdf.internal.pageSize.getHeight();
pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST');
const { pathname, id, filename } = getName();
const data = pdf.__private__.getArrayBuffer(pdf.__private__.buildDocument());
await invoke('download', { name: `download/pdf/${Date.now().toString(36)}.pdf`, blob: Array.from(new Uint8Array(data)) });
await invoke('download_list');
await invoke('download', { name: `download/pdf/${id}.pdf`, blob: Array.from(new Uint8Array(data)) });
await invoke('download_list', { pathname, filename, id });
}
function getName() {
const id = uid().toString(36);
const name = document.querySelector('nav .overflow-y-auto a.hover\\:bg-gray-800')?.innerText?.trim() || '';
return { filename: name ? name : id, id, pathname: 'chat.download.json' };
}
class Elements {

View File

@ -1,4 +1,7 @@
import { useState, useCallback } from 'react';
import { FC, useState, useCallback } from 'react';
import { Input } from 'antd';
import { DISABLE_AUTO_COMPLETE } from '@/utils';
export default function useColumns(columns: any[] = []) {
const [opType, setOpType] = useState('');
@ -41,4 +44,40 @@ export default function useColumns(columns: any[] = []) {
setExtra,
opExtra,
};
}
}
interface EditRowProps {
rowKey: string;
row: Record<string, any>;
actions: any;
}
export const EditRow: FC<EditRowProps> = ({ rowKey, row, actions }) => {
const [isEdit, setEdit] = useState(false);
const [val, setVal] = useState(row[rowKey]);
const handleEdit = () => {
setEdit(true);
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setVal(e.target.value)
};
const handleSave = () => {
setEdit(false);
row[rowKey] = val;
actions?.setRecord(row, 'rowedit')
};
return isEdit
? (
<Input.TextArea
value={val}
rows={1}
onChange={handleChange}
{...DISABLE_AUTO_COMPLETE}
onPressEnter={handleSave}
/>
)
: (
<div className='rowedit' onClick={handleEdit}>{val}</div>
);
};

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { readJSON } from '@/utils';
import { readJSON, writeJSON } from '@/utils';
import useInit from '@/hooks/useInit';
export default function useJson<T>(file: string) {
@ -11,7 +11,12 @@ export default function useJson<T>(file: string) {
setData(data);
};
const updateJson = async (data: any) => {
await writeJSON(file, data);
await refreshJson();
};
useInit(refreshJson);
return { json, refreshJson };
return { json, refreshJson, updateJson };
}

View File

@ -4,14 +4,35 @@ import type { TableRowSelection } from 'antd/es/table/interface';
import { safeKey } from '@/hooks/useData';
export default function useTableRowSelection() {
type rowSelectionOptions = {
key: 'id' | string;
rowType: 'id' | 'row' | 'all';
}
export function useTableRowSelection(options: Partial<rowSelectionOptions> = {}) {
const { key = 'id', rowType = 'id' } = options;
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [selectedRowIDs, setSelectedRowIDs] = useState<string[]>([]);
const [selectedRows, setSelectedRows] = useState<Record<string|symbol, any>[]>([]);
const onSelectChange = (newSelectedRowKeys: React.Key[], selectedRows: Record<string|symbol, any>) => {
const keys = selectedRows.map((i: any) => i[safeKey]);
setSelectedRowIDs(keys);
const onSelectChange = (newSelectedRowKeys: React.Key[], newSelectedRows: Record<string|symbol, any>[]) => {
const keys = newSelectedRows.map((i: any) => i[safeKey] || i[key]);
setSelectedRowKeys(newSelectedRowKeys);
if (rowType === 'id') {
setSelectedRowIDs(keys);
}
if (rowType === 'row') {
setSelectedRows(newSelectedRows);
}
if (rowType === 'all') {
setSelectedRowIDs(keys);
setSelectedRows(newSelectedRows);
}
};
const rowReset = () => {
setSelectedRowKeys([]);
setSelectedRowIDs([]);
setSelectedRows([]);
};
const rowSelection: TableRowSelection<Record<string, any>> = {
@ -24,14 +45,14 @@ export default function useTableRowSelection() {
],
};
return { rowSelection, selectedRowIDs };
return { rowSelection, selectedRowIDs, selectedRows, rowReset };
}
export const TABLE_PAGINATION = {
hideOnSinglePage: true,
showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 5,
defaultPageSize: 10,
pageSizeOptions: [5, 10, 15, 20],
showTotal: (total: number) => <span>Total {total} items</span>,
};

19
src/main.scss vendored
View File

@ -31,10 +31,27 @@ html, body {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.ellipsis-line {
display: inline-block;
width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rowedit {
padding: 2px 5px;
&:hover {
box-shadow: 0 0 2px rgba(237, 122, 60, 0.8);
border-radius: 4px;
}
}
.chat-add-btn {
margin-bottom: 5px;
}

View File

@ -1,37 +1,57 @@
import { useState } from 'react';
import { Tag, Space, Popconfirm } from 'antd';
import { path, shell } from '@tauri-apps/api';
import { fmtDate } from '@/utils';
import { EditRow } from '@/hooks/useColumns';
import useInit from '@/hooks/useInit';
import { fmtDate, chatRoot } from '@/utils';
const colorMap: any = {
pdf: 'blue',
png: 'orange',
}
export const syncColumns = () => [
export const downloadColumns = () => [
{
title: 'Name',
dataIndex: 'name',
fixed: 'left',
key: 'name',
width: 240,
render: (_: string, row: any, actions: any) => (
<EditRow rowKey="name" row={row} actions={actions} />
),
},
{
title: 'Extension',
dataIndex: 'ext',
key: 'ext',
width: 120,
render: (v: string) => <Tag color={colorMap[v]}>{v}</Tag>,
},
{
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: 150,
render: (_: any, row: any, actions: any) => {
return (
<Space>
<a onClick={() => actions.setRecord(row, 'view')}>View</a>
<a onClick={() => actions.setRecord(row, 'preview')}>Preview</a>
<Popconfirm
title="Are you sure to delete this file?"
onConfirm={() => actions.setRecord(row, 'delete')}
@ -46,9 +66,15 @@ export const syncColumns = () => [
}
];
// {
// id: '',
// name: '',
// type: '.png',
// created: '2022.01.01',
// }
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(), 'download', isImg ? 'img' : row.ext, row.id) + `.${row.ext}`;
}

View File

@ -1,12 +0,0 @@
.chat-table-tip, .chat-table-btns {
display: flex;
justify-content: space-between;
}
.chat-table-btns {
margin-bottom: 5px;
.num {
margin-left: 10px;
}
}

View File

@ -1,14 +1,14 @@
import { useEffect, useState } from 'react';
import { Table, Modal } from 'antd';
import { path, shell, fs } from '@tauri-apps/api';
import { Table, Modal, Popconfirm, Button, message } from 'antd';
import { invoke, path, shell, fs } from '@tauri-apps/api';
import useInit from '@/hooks/useInit';
import useJson from '@/hooks/useJson';
import useData from '@/hooks/useData';
import useColumns from '@/hooks/useColumns';
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { chatRoot, CHAT_DOWNLOAD_JSON } from '@/utils';
import { syncColumns } from './config';
import './index.scss';
import { downloadColumns } from './config';
function renderFile(buff: Uint8Array, type: string) {
const renderType = {
@ -19,52 +19,124 @@ function renderFile(buff: Uint8Array, type: string) {
}
export default function SyncPrompts() {
const { rowSelection, selectedRowIDs } = useTable();
const { columns, ...opInfo } = useColumns(syncColumns());
const [downloadPath, setDownloadPath] = useState('');
const { json } = useJson<any[]>(CHAT_DOWNLOAD_JSON);
const [source, setSource] = useState('');
const [isVisible, setVisible] = useState(false);
const { opData, opInit, opReplace, opSafeKey } = useData([]);
const { columns, ...opInfo } = useColumns(downloadColumns());
const { rowSelection, selectedRows, rowReset } = useTableRowSelection({ rowType: 'row' });
const { json, refreshJson, updateJson } = useJson<any[]>(CHAT_DOWNLOAD_JSON);
const selectedItems = rowSelection.selectedRowKeys || [];
useInit(async () => {
const file = await path.join(await chatRoot(), 'chat.download.json');
const file = await path.join(await chatRoot(), CHAT_DOWNLOAD_JSON);
setDownloadPath(file);
});
useEffect(() => {
if (!json || json.length <= 0) return;
opInit(json);
}, [json?.length]);
useEffect(() => {
if (!opInfo.opType) return;
(async () => {
const record = opInfo?.opRecord;
const isImg = ['png'].includes(record?.ext);
const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : record?.ext, `${record?.id}.${record?.ext}`);
if (opInfo.opType === 'view') {
if (opInfo.opType === 'preview') {
const data = await fs.readBinaryFile(file);
const sourceData = renderFile(data, record?.ext);
setSource(sourceData);
setVisible(true);
return;
}
if (opInfo.opType === 'file') {
await shell.open(file);
}
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 downloadDir = await path.join(await chatRoot(), 'download');
await fs.removeDir(downloadDir, { recursive: true });
await handleRefresh();
rowReset();
message.success('All files have been cleared!');
return;
}
const rows = selectedRows.map(async (i) => {
const isImg = ['png'].includes(i?.ext);
const file = await path.join(await chatRoot(), 'download', isImg ? 'img' : i?.ext, `${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_DOWNLOAD_JSON });
refreshJson();
};
const handleCancel = () => {
setVisible(false);
opInfo.resetRecord();
};
return (
<div>
<div className="chat-table-btns">
<div>
{selectedItems.length > 0 && (
<>
<Popconfirm
overlayStyle={{ width: 250 }}
title="Sync will overwrite the previous data, confirm to sync?"
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(downloadPath)} title={downloadPath}>{downloadPath}</a></div>
</div>
</div>
<Table
rowKey="name"
rowKey="id"
columns={columns}
scroll={{ x: 'auto' }}
dataSource={json}
scroll={{ x: 800 }}
dataSource={opData}
rowSelection={rowSelection}
pagination={TABLE_PAGINATION}
/>
<Modal
open={isVisible}
onCancel={() => setVisible(false)}
title={<div>{opInfo?.opRecord?.name || ''}</div>}
onCancel={handleCancel}
footer={false}
destroyOnClose
>

View File

@ -1,4 +1,4 @@
import { Switch, Tag, Tooltip } from 'antd';
import { Table, Switch, Tag } from 'antd';
import { genCmd } from '@/utils';
@ -35,13 +35,14 @@ export const syncColumns = () => [
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
),
},
Table.EXPAND_COLUMN,
{
title: 'Prompt',
dataIndex: 'prompt',
key: 'prompt',
// width: 300,
render: (v: string) => (
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
<span className="chat-prompts-val">{v}</span>
),
},
];

View File

@ -6,7 +6,7 @@ import useInit from '@/hooks/useInit';
import useData from '@/hooks/useData';
import useColumns from '@/hooks/useColumns';
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { fmtDate, chatRoot } from '@/utils';
import { syncColumns } from './config';
import './index.scss';
@ -14,7 +14,7 @@ import './index.scss';
const promptsURL = 'https://github.com/f/awesome-chatgpt-prompts/blob/main/prompts.csv';
export default function SyncPrompts() {
const { rowSelection, selectedRowIDs } = useTable();
const { rowSelection, selectedRowIDs } = useTableRowSelection();
const [jsonPath, setJsonPath] = useState('');
const { modelJson, modelSet } = useChatModel('sync_prompts');
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
@ -93,6 +93,7 @@ export default function SyncPrompts() {
dataSource={opData}
rowSelection={rowSelection}
pagination={TABLE_PAGINATION}
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}}
/>
</div>
)

View File

@ -1,4 +1,4 @@
import { Switch, Tag, Tooltip } from 'antd';
import { Switch, Tag, Table } from 'antd';
import { genCmd } from '@/utils';
@ -37,13 +37,14 @@ export const syncColumns = () => [
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
),
},
Table.EXPAND_COLUMN,
{
title: 'Prompt',
dataIndex: 'prompt',
key: 'prompt',
// width: 300,
render: (v: string) => (
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
<span className="chat-prompts-val">{v}</span>
),
},
];

View File

@ -7,7 +7,7 @@ import { shell, path } from '@tauri-apps/api';
import useColumns from '@/hooks/useColumns';
import useData from '@/hooks/useData';
import { useCacheModel } from '@/hooks/useChatModel';
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { fmtDate, chatRoot } from '@/utils';
import { getPath } from '@/view/model/SyncCustom/config';
import { syncColumns } from './config';
@ -19,7 +19,7 @@ export default function SyncRecord() {
const [jsonPath, setJsonPath] = useState('');
const state = location?.state;
const { rowSelection, selectedRowIDs } = useTable();
const { rowSelection, selectedRowIDs } = useTableRowSelection();
const { modelCacheJson, modelCacheSet } = useCacheModel(jsonPath);
const { opData, opInit, opReplace, opReplaceItems, opSafeKey } = useData([]);
const { columns, ...opInfo } = useColumns(syncColumns());
@ -79,6 +79,7 @@ export default function SyncRecord() {
dataSource={opData}
rowSelection={rowSelection}
pagination={TABLE_PAGINATION}
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}}
/>
</div>
)

View File

@ -1,4 +1,4 @@
import { Tag, Switch, Tooltip, Space, Popconfirm } from 'antd';
import { Tag, Switch, Space, Popconfirm, Table } from 'antd';
export const modelColumns = () => [
{
@ -33,13 +33,14 @@ export const modelColumns = () => [
<Switch checked={v} onChange={(v) => action.setRecord({ ...row, enable: v }, 'enable')} />
),
},
Table.EXPAND_COLUMN,
{
title: 'Prompt',
dataIndex: 'prompt',
key: 'prompt',
width: 300,
render: (v: string) => (
<Tooltip overlayInnerStyle={{ width: 350 }} title={v}><span className="chat-prompts-val">{v}</span></Tooltip>
<span className="chat-prompts-val">{v}</span>
),
},
{

View File

@ -6,13 +6,13 @@ import useInit from '@/hooks/useInit';
import useData from '@/hooks/useData';
import useChatModel, { useCacheModel } from '@/hooks/useChatModel';
import useColumns from '@/hooks/useColumns';
import useTable, { TABLE_PAGINATION } from '@/hooks/useTable';
import { useTableRowSelection, TABLE_PAGINATION } from '@/hooks/useTable';
import { chatRoot, fmtDate } from '@/utils';
import { modelColumns } from './config';
import UserCustomForm from './Form';
export default function LanguageModel() {
const { rowSelection, selectedRowIDs } = useTable();
const { rowSelection, selectedRowIDs } = useTableRowSelection();
const [isVisible, setVisible] = useState(false);
const [jsonPath, setJsonPath] = useState('');
const { modelJson, modelSet } = useChatModel('user_custom');
@ -123,6 +123,7 @@ export default function LanguageModel() {
dataSource={opData}
rowSelection={rowSelection}
pagination={TABLE_PAGINATION}
expandable={{expandedRowRender: (record) => <div style={{ padding: 10 }}>{record.prompt}</div>}}
/>
<Modal
open={isVisible}