Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ dmypy.json

*.bin
deploy/aperag/production.yaml
documents

# ssl file
ssl_file
Expand Down
94 changes: 94 additions & 0 deletions web/src/pages/Collections/documents/addUrl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { createCollectionUrls } from '@/services';
import { TypesCollection } from '@/types';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { FormattedMessage } from '@umijs/max';
import { Button, Col, Form, Input, Modal, Row } from 'antd';
import { useEffect, useState } from 'react';

type Props = {
collection: TypesCollection;
onSuccess: () => void;
};

export default ({ collection, onSuccess }: Props) => {
const [loading, setLoading] = useState<boolean>(false);
const [visible, setVisible] = useState<boolean>(false);
const [form] = Form.useForm();

const onSave = async () => {
const values = await form.validateFields();
if (!collection.id) return;
const { code } = await createCollectionUrls(collection.id, values);
if (code === '200') {
setVisible(false);
onSuccess();
}
setLoading(false);
};

useEffect(() => {
form.setFieldValue('urls', [{}]);
}, []);

return (
<>
<Button
onClick={() => setVisible(true)}
disabled={collection?.system}
type="primary"
loading={loading}
>
<FormattedMessage id="action.documents.addUrl" />
</Button>
<Modal
title={<FormattedMessage id="action.documents.addUrl" />}
open={visible}
onCancel={() => setVisible(false)}
onOk={() => onSave()}
forceRender
>
<Form form={form} style={{ marginTop: 30 }}>
<Form.List name="urls">
{(fields, { add, remove }) => {
return (
<>
{fields.map(({ key, name, ...restField }) => (
<Row key={key} gutter={[8, 8]}>
<Col span={21}>
<Form.Item
{...restField}
name={[name, 'url']}
style={{ marginBottom: 8 }}
rules={[
{ required: true, message: 'url is required' },
]}
>
<Input placeholder="URL" />
</Form.Item>
</Col>
<Col span={3}>
<Button block onClick={() => remove(name)}>
<MinusCircleOutlined />
</Button>
</Col>
</Row>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
>
Add URL
</Button>
</Form.Item>
</>
);
}}
</Form.List>
</Form>
</Modal>
</>
);
};
173 changes: 173 additions & 0 deletions web/src/pages/Collections/documents/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { FormattedMessage, useModel, useParams, useIntl, history } from '@umijs/max';
import {
App,
Pagination,
Card,
Upload,
UploadProps,
} from 'antd';
import _ from 'lodash';
import { useEffect, useState } from 'react';
import AddUrl from './addUrl';
import Documentlist from '@/components/DocumentTable';

export default () => {
const { user } = useModel('user');
const { collectionId, page } = useParams();
const [searchKey, setSearchKey] = useState<string>('');
const [pageSize, setPageSize] = useState<number>(10);
const [pageNumber, setPageNumber] = useState<number>(1);
const { modal, message } = App.useApp();
const { collections, getCollection } = useModel('collection');
const { documents, documentLoading, totalCount,getDocuments,deleteDocument } = useModel('document');
const [uploading, setUploading] = useState<boolean>(false);
const [ collection, setCollection ] = useState<any>();
const [ readonly, setReadonly ] = useState(false);
const config = collection?.config;

useEffect(()=>{
const collectionDetail = getCollection(collectionId);
setCollection(collectionDetail);
setReadonly(collectionDetail?.system && !user?.is_admin);
}, [collections])

const dataSource = documents?.filter((c) => {
return c.name?.match(new RegExp(searchKey, 'i')) || _.isEmpty(searchKey);
});

const { formatMessage } = useIntl();

const getDocument = async (page, page_size) => {
const pageId = page || pageNumber;
const page_Size = page_size || pageSize;
getDocuments(String(collectionId), pageId, page_Size);
setPageNumber(pageId);
};

const changePage = (page, pageSize) => {
setPageSize(pageSize);
// getDocument(page, pageSize);
history.replace(`/collections/${collectionId}/documents/${page}`);
};

const onDelete = (record) => {
if (!collectionId) return;

let msg = [];

if(!_.isArray(record)){
record = [record];
}

record.map((item, idx)=>{
msg.push((idx+1) + '. ' + item.name);
});

msg = msg.join('<br/>');

modal.confirm({
title: formatMessage({ id: 'text.delete.help' }),
content: <>{formatMessage({ id: 'text.documents' })}: <div dangerouslySetInnerHTML={{ __html: msg }} /><br/>{formatMessage({ id: 'text.delete.confirm' })}</>,
onOk: async () => {
const items = [];
record.map((item)=>items.push(item.id));
await deleteDocument(collectionId, items);
setTimeout(() => {
getDocument();
});
},
okButtonProps: {
danger: true,
},
});
};

const SUPPORTED_DOC_EXTENSIONS=['.pdf','.doc','.docx','.ppt','.pptx','.csv','.xls','.xlsx','.epub','.md','.mbox','.ipynb','.txt','.htm','.html'];
const SUPPORTED_MEDIA_EXTENSIONS=['.jpg','.jpeg','.png','.webm','.mp3','.mp4','.mpeg','.mpga','.m4a','.wav'];
const SUPPORTED_COMPRESSED_EXTENSIONS=['.zip','.rar', '.r00','.7z','.tar', '.gz', '.xz', '.bz2', '.tar.gz', '.tar.xz', '.tar.bz2', '.tar.7z'];

const uploadProps: UploadProps = {
name: 'file',
multiple: true,
disabled: readonly,
action: `/api/v1/collections/${collectionId}/documents`,
data: {},
showUploadList: false,
headers: {
Authorization: 'Bearer ' + user?.__raw,
},
accept: SUPPORTED_DOC_EXTENSIONS.concat(SUPPORTED_MEDIA_EXTENSIONS).concat(SUPPORTED_COMPRESSED_EXTENSIONS).join(','),
onChange(info) {
const { status, response } = info.file;
if (status === 'done') {
if (response.code === '200') {
getDocument();
} else {
message.error(response.message || 'update error');
}
setUploading(false);
} else {
setUploading(true);
if (status === 'error') {
message.error('upload error');
}
}
},
};

useEffect(() => {
getDocument(parseInt(page));
}, [history.location.pathname]);

return (
<div className="border-block document">
<Card bordered={false}>
<div className='hd'>
<div className='title'>

</div>
<div className="action">
<label className="search">
<input
type="text"
onChange={(e) => setSearchKey(e.currentTarget.value)}
/>
</label>
{!readonly && config?.source === 'system' ? (
<Upload {...uploadProps} disabled={(readonly || uploading)}>
{/* eslint-disable-next-line react/button-has-type */}
<button disabled={readonly} className={uploading ? 'waiting' : ''}>
<FormattedMessage id="action.documents.add" />
</button>
</Upload>
) : null}
{config?.source === 'url' ? (
<AddUrl onSuccess={() => getDocument()} collection={collection} />
) : null}
</div>
</div>
<div className='bd'>
<Documentlist list={dataSource} getDocuments={()=> getDocument()} onDelete={onDelete} loading={documentLoading} selection={!readonly && totalCount} editable={!readonly}/>
{totalCount ? (
<div className='pagination'>
<div className='wrap'>
<Pagination
simple
total={totalCount}
pageSize={pageSize}
current={pageNumber}
showQuickJumper
showSizeChanger
onChange={changePage}
hideOnSinglePage={true}
responsive={true}
showTotal={(totalItems) => `${totalItems}${formatMessage({id:'text.items'})}`}
/>
</div>
</div>
):''}
</div>
</Card>
</div>
);
};
116 changes: 116 additions & 0 deletions web/src/pages/Collections/documents/labelEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { UpdateDocument } from '@/services/documents';
import { TypesDocument, TypesDocumentConfig } from '@/types';
import { stringifyConfig } from '@/utils/configParse';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { FormattedMessage } from '@umijs/max';
import { App, Button, Col, Form, Input, Modal, Row, Typography } from 'antd';
import { useEffect } from 'react';

type Props = {
disabled?: boolean;
collectionId?: string;
document: TypesDocument;
onSuccess: (document: TypesDocument) => void;
};

export default ({ collectionId, document, onSuccess, show, cancel }: Props) => {
const [form] = Form.useForm();
const { message } = App.useApp();

const onSave = async () => {
if (!collectionId) return;

const { labels } = await form.validateFields();
document.config.labels = labels;

document.config = stringifyConfig(document.config) as TypesDocumentConfig;
const res = await UpdateDocument(collectionId, document.id, document);

if (res.code === '200') {
onSuccess(document);
message.success('update success');
} else {
message.success('update error');
}
};

useEffect(() => {
form.setFieldsValue({
labels: document.config?.labels?.length ? document.config.labels : [{}],
});
}, [document]);

return (
<>
<Modal
title={<FormattedMessage id="text.labels" />}
open={show}
onCancel={() => cancel()}
onOk={async () => {onSave()}}
forceRender
>
<br />
<Form form={form}>
<Form.List name="labels">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Row key={key} gutter={[8, 8]}>
<Col span="11">
<Form.Item
{...restField}
name={[name, 'key']}
rules={[
{ required: true, message: 'Key is required.' },
]}
style={{ marginBottom: 8 }}
>
<Input placeholder="Key" />
</Form.Item>
</Col>
<Col span="11">
<Form.Item
{...restField}
name={[name, 'value']}
rules={[
{ required: true, message: 'Value is required.' },
]}
style={{ marginBottom: 8 }}
>
<Input placeholder="Value" />
</Form.Item>
</Col>
<Col span="2">
<Button
onClick={() => remove(name)}
block
style={{ padding: '4px', textAlign: 'center' }}
>
<Typography.Text type="secondary">
<MinusCircleOutlined />
</Typography.Text>
</Button>
</Col>
</Row>
))}
<br />
<Button
type="dashed"
onClick={() => add()}
block
icon={
<Typography.Text type="secondary">
<PlusOutlined />
</Typography.Text>
}
/>
</>
)}
</Form.List>
</Form>
<br />
<br />
</Modal>
</>
);
};
Loading