Skip to content

Commit 8f62071

Browse files
authored
Merge pull request #669 from apecloud/feat/aperag_console
feat: aperag console
2 parents 4f9753c + ea92c8d commit 8f62071

File tree

4 files changed

+383
-1
lines changed

4 files changed

+383
-1
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ dmypy.json
145145

146146
*.bin
147147
deploy/aperag/production.yaml
148-
documents
149148

150149
# ssl file
151150
ssl_file
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { createCollectionUrls } from '@/services';
2+
import { TypesCollection } from '@/types';
3+
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
4+
import { FormattedMessage } from '@umijs/max';
5+
import { Button, Col, Form, Input, Modal, Row } from 'antd';
6+
import { useEffect, useState } from 'react';
7+
8+
type Props = {
9+
collection: TypesCollection;
10+
onSuccess: () => void;
11+
};
12+
13+
export default ({ collection, onSuccess }: Props) => {
14+
const [loading, setLoading] = useState<boolean>(false);
15+
const [visible, setVisible] = useState<boolean>(false);
16+
const [form] = Form.useForm();
17+
18+
const onSave = async () => {
19+
const values = await form.validateFields();
20+
if (!collection.id) return;
21+
const { code } = await createCollectionUrls(collection.id, values);
22+
if (code === '200') {
23+
setVisible(false);
24+
onSuccess();
25+
}
26+
setLoading(false);
27+
};
28+
29+
useEffect(() => {
30+
form.setFieldValue('urls', [{}]);
31+
}, []);
32+
33+
return (
34+
<>
35+
<Button
36+
onClick={() => setVisible(true)}
37+
disabled={collection?.system}
38+
type="primary"
39+
loading={loading}
40+
>
41+
<FormattedMessage id="action.documents.addUrl" />
42+
</Button>
43+
<Modal
44+
title={<FormattedMessage id="action.documents.addUrl" />}
45+
open={visible}
46+
onCancel={() => setVisible(false)}
47+
onOk={() => onSave()}
48+
forceRender
49+
>
50+
<Form form={form} style={{ marginTop: 30 }}>
51+
<Form.List name="urls">
52+
{(fields, { add, remove }) => {
53+
return (
54+
<>
55+
{fields.map(({ key, name, ...restField }) => (
56+
<Row key={key} gutter={[8, 8]}>
57+
<Col span={21}>
58+
<Form.Item
59+
{...restField}
60+
name={[name, 'url']}
61+
style={{ marginBottom: 8 }}
62+
rules={[
63+
{ required: true, message: 'url is required' },
64+
]}
65+
>
66+
<Input placeholder="URL" />
67+
</Form.Item>
68+
</Col>
69+
<Col span={3}>
70+
<Button block onClick={() => remove(name)}>
71+
<MinusCircleOutlined />
72+
</Button>
73+
</Col>
74+
</Row>
75+
))}
76+
<Form.Item>
77+
<Button
78+
type="dashed"
79+
onClick={() => add()}
80+
block
81+
icon={<PlusOutlined />}
82+
>
83+
Add URL
84+
</Button>
85+
</Form.Item>
86+
</>
87+
);
88+
}}
89+
</Form.List>
90+
</Form>
91+
</Modal>
92+
</>
93+
);
94+
};
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { FormattedMessage, useModel, useParams, useIntl, history } from '@umijs/max';
2+
import {
3+
App,
4+
Pagination,
5+
Card,
6+
Upload,
7+
UploadProps,
8+
} from 'antd';
9+
import _ from 'lodash';
10+
import { useEffect, useState } from 'react';
11+
import AddUrl from './addUrl';
12+
import Documentlist from '@/components/DocumentTable';
13+
14+
export default () => {
15+
const { user } = useModel('user');
16+
const { collectionId, page } = useParams();
17+
const [searchKey, setSearchKey] = useState<string>('');
18+
const [pageSize, setPageSize] = useState<number>(10);
19+
const [pageNumber, setPageNumber] = useState<number>(1);
20+
const { modal, message } = App.useApp();
21+
const { collections, getCollection } = useModel('collection');
22+
const { documents, documentLoading, totalCount,getDocuments,deleteDocument } = useModel('document');
23+
const [uploading, setUploading] = useState<boolean>(false);
24+
const [ collection, setCollection ] = useState<any>();
25+
const [ readonly, setReadonly ] = useState(false);
26+
const config = collection?.config;
27+
28+
useEffect(()=>{
29+
const collectionDetail = getCollection(collectionId);
30+
setCollection(collectionDetail);
31+
setReadonly(collectionDetail?.system && !user?.is_admin);
32+
}, [collections])
33+
34+
const dataSource = documents?.filter((c) => {
35+
return c.name?.match(new RegExp(searchKey, 'i')) || _.isEmpty(searchKey);
36+
});
37+
38+
const { formatMessage } = useIntl();
39+
40+
const getDocument = async (page, page_size) => {
41+
const pageId = page || pageNumber;
42+
const page_Size = page_size || pageSize;
43+
getDocuments(String(collectionId), pageId, page_Size);
44+
setPageNumber(pageId);
45+
};
46+
47+
const changePage = (page, pageSize) => {
48+
setPageSize(pageSize);
49+
// getDocument(page, pageSize);
50+
history.replace(`/collections/${collectionId}/documents/${page}`);
51+
};
52+
53+
const onDelete = (record) => {
54+
if (!collectionId) return;
55+
56+
let msg = [];
57+
58+
if(!_.isArray(record)){
59+
record = [record];
60+
}
61+
62+
record.map((item, idx)=>{
63+
msg.push((idx+1) + '. ' + item.name);
64+
});
65+
66+
msg = msg.join('<br/>');
67+
68+
modal.confirm({
69+
title: formatMessage({ id: 'text.delete.help' }),
70+
content: <>{formatMessage({ id: 'text.documents' })}: <div dangerouslySetInnerHTML={{ __html: msg }} /><br/>{formatMessage({ id: 'text.delete.confirm' })}</>,
71+
onOk: async () => {
72+
const items = [];
73+
record.map((item)=>items.push(item.id));
74+
await deleteDocument(collectionId, items);
75+
setTimeout(() => {
76+
getDocument();
77+
});
78+
},
79+
okButtonProps: {
80+
danger: true,
81+
},
82+
});
83+
};
84+
85+
const SUPPORTED_DOC_EXTENSIONS=['.pdf','.doc','.docx','.ppt','.pptx','.csv','.xls','.xlsx','.epub','.md','.mbox','.ipynb','.txt','.htm','.html'];
86+
const SUPPORTED_MEDIA_EXTENSIONS=['.jpg','.jpeg','.png','.webm','.mp3','.mp4','.mpeg','.mpga','.m4a','.wav'];
87+
const SUPPORTED_COMPRESSED_EXTENSIONS=['.zip','.rar', '.r00','.7z','.tar', '.gz', '.xz', '.bz2', '.tar.gz', '.tar.xz', '.tar.bz2', '.tar.7z'];
88+
89+
const uploadProps: UploadProps = {
90+
name: 'file',
91+
multiple: true,
92+
disabled: readonly,
93+
action: `/api/v1/collections/${collectionId}/documents`,
94+
data: {},
95+
showUploadList: false,
96+
headers: {
97+
Authorization: 'Bearer ' + user?.__raw,
98+
},
99+
accept: SUPPORTED_DOC_EXTENSIONS.concat(SUPPORTED_MEDIA_EXTENSIONS).concat(SUPPORTED_COMPRESSED_EXTENSIONS).join(','),
100+
onChange(info) {
101+
const { status, response } = info.file;
102+
if (status === 'done') {
103+
if (response.code === '200') {
104+
getDocument();
105+
} else {
106+
message.error(response.message || 'update error');
107+
}
108+
setUploading(false);
109+
} else {
110+
setUploading(true);
111+
if (status === 'error') {
112+
message.error('upload error');
113+
}
114+
}
115+
},
116+
};
117+
118+
useEffect(() => {
119+
getDocument(parseInt(page));
120+
}, [history.location.pathname]);
121+
122+
return (
123+
<div className="border-block document">
124+
<Card bordered={false}>
125+
<div className='hd'>
126+
<div className='title'>
127+
128+
</div>
129+
<div className="action">
130+
<label className="search">
131+
<input
132+
type="text"
133+
onChange={(e) => setSearchKey(e.currentTarget.value)}
134+
/>
135+
</label>
136+
{!readonly && config?.source === 'system' ? (
137+
<Upload {...uploadProps} disabled={(readonly || uploading)}>
138+
{/* eslint-disable-next-line react/button-has-type */}
139+
<button disabled={readonly} className={uploading ? 'waiting' : ''}>
140+
<FormattedMessage id="action.documents.add" />
141+
</button>
142+
</Upload>
143+
) : null}
144+
{config?.source === 'url' ? (
145+
<AddUrl onSuccess={() => getDocument()} collection={collection} />
146+
) : null}
147+
</div>
148+
</div>
149+
<div className='bd'>
150+
<Documentlist list={dataSource} getDocuments={()=> getDocument()} onDelete={onDelete} loading={documentLoading} selection={!readonly && totalCount} editable={!readonly}/>
151+
{totalCount ? (
152+
<div className='pagination'>
153+
<div className='wrap'>
154+
<Pagination
155+
simple
156+
total={totalCount}
157+
pageSize={pageSize}
158+
current={pageNumber}
159+
showQuickJumper
160+
showSizeChanger
161+
onChange={changePage}
162+
hideOnSinglePage={true}
163+
responsive={true}
164+
showTotal={(totalItems) => `${totalItems}${formatMessage({id:'text.items'})}`}
165+
/>
166+
</div>
167+
</div>
168+
):''}
169+
</div>
170+
</Card>
171+
</div>
172+
);
173+
};
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { UpdateDocument } from '@/services/documents';
2+
import { TypesDocument, TypesDocumentConfig } from '@/types';
3+
import { stringifyConfig } from '@/utils/configParse';
4+
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
5+
import { FormattedMessage } from '@umijs/max';
6+
import { App, Button, Col, Form, Input, Modal, Row, Typography } from 'antd';
7+
import { useEffect } from 'react';
8+
9+
type Props = {
10+
disabled?: boolean;
11+
collectionId?: string;
12+
document: TypesDocument;
13+
onSuccess: (document: TypesDocument) => void;
14+
};
15+
16+
export default ({ collectionId, document, onSuccess, show, cancel }: Props) => {
17+
const [form] = Form.useForm();
18+
const { message } = App.useApp();
19+
20+
const onSave = async () => {
21+
if (!collectionId) return;
22+
23+
const { labels } = await form.validateFields();
24+
document.config.labels = labels;
25+
26+
document.config = stringifyConfig(document.config) as TypesDocumentConfig;
27+
const res = await UpdateDocument(collectionId, document.id, document);
28+
29+
if (res.code === '200') {
30+
onSuccess(document);
31+
message.success('update success');
32+
} else {
33+
message.success('update error');
34+
}
35+
};
36+
37+
useEffect(() => {
38+
form.setFieldsValue({
39+
labels: document.config?.labels?.length ? document.config.labels : [{}],
40+
});
41+
}, [document]);
42+
43+
return (
44+
<>
45+
<Modal
46+
title={<FormattedMessage id="text.labels" />}
47+
open={show}
48+
onCancel={() => cancel()}
49+
onOk={async () => {onSave()}}
50+
forceRender
51+
>
52+
<br />
53+
<Form form={form}>
54+
<Form.List name="labels">
55+
{(fields, { add, remove }) => (
56+
<>
57+
{fields.map(({ key, name, ...restField }) => (
58+
<Row key={key} gutter={[8, 8]}>
59+
<Col span="11">
60+
<Form.Item
61+
{...restField}
62+
name={[name, 'key']}
63+
rules={[
64+
{ required: true, message: 'Key is required.' },
65+
]}
66+
style={{ marginBottom: 8 }}
67+
>
68+
<Input placeholder="Key" />
69+
</Form.Item>
70+
</Col>
71+
<Col span="11">
72+
<Form.Item
73+
{...restField}
74+
name={[name, 'value']}
75+
rules={[
76+
{ required: true, message: 'Value is required.' },
77+
]}
78+
style={{ marginBottom: 8 }}
79+
>
80+
<Input placeholder="Value" />
81+
</Form.Item>
82+
</Col>
83+
<Col span="2">
84+
<Button
85+
onClick={() => remove(name)}
86+
block
87+
style={{ padding: '4px', textAlign: 'center' }}
88+
>
89+
<Typography.Text type="secondary">
90+
<MinusCircleOutlined />
91+
</Typography.Text>
92+
</Button>
93+
</Col>
94+
</Row>
95+
))}
96+
<br />
97+
<Button
98+
type="dashed"
99+
onClick={() => add()}
100+
block
101+
icon={
102+
<Typography.Text type="secondary">
103+
<PlusOutlined />
104+
</Typography.Text>
105+
}
106+
/>
107+
</>
108+
)}
109+
</Form.List>
110+
</Form>
111+
<br />
112+
<br />
113+
</Modal>
114+
</>
115+
);
116+
};

0 commit comments

Comments
 (0)