refactor: Add frontend code to DB-GPT (#912)

This commit is contained in:
katakuri
2023-12-11 00:05:42 +08:00
committed by GitHub
parent b8dc9cf11e
commit 43190ca333
189 changed files with 19179 additions and 16 deletions

View File

@@ -0,0 +1,192 @@
import React, { useEffect, useState } from 'react';
import { Modal, Tabs, Button, Input, Form, Col, Row, Spin } from 'antd';
import { useTranslation } from 'react-i18next';
import { AlertFilled, BookOutlined, FileSearchOutlined } from '@ant-design/icons';
import { apiInterceptors, getArguments, saveArguments } from '@/client/api';
import { IArguments, ISpace } from '@/types/knowledge';
const { TextArea } = Input;
interface IProps {
space: ISpace;
argumentsShow: boolean;
setArgumentsShow: (argumentsShow: boolean) => void;
}
export default function ArgumentsModal({ space, argumentsShow, setArgumentsShow }: IProps) {
const { t } = useTranslation();
const [newSpaceArguments, setNewSpaceArguments] = useState<IArguments | null>();
const [spinning, setSpinning] = useState<boolean>(false);
const fetchArguments = async () => {
const [_, data] = await apiInterceptors(getArguments(space.name));
setNewSpaceArguments(data);
};
useEffect(() => {
fetchArguments();
}, [space.name]);
const renderEmbeddingForm = () => {
return (
<Row gutter={24}>
<Col span={12} offset={0}>
<Form.Item<IArguments> tooltip={t(`the_top_k_vectors`)} rules={[{ required: true }]} label={t('topk')} name={['embedding', 'topk']}>
<Input className="mb-5 h-12" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item<IArguments>
tooltip={t(`Set_a_threshold_score`)}
rules={[{ required: true }]}
label={t('recall_score')}
name={['embedding', 'recall_score']}
>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_owner')} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item<IArguments> tooltip={t(`Recall_Type`)} rules={[{ required: true }]} label={t('recall_type')} name={['embedding', 'recall_type']}>
<Input className="mb-5 h-12" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item<IArguments> tooltip={t(`A_model_used`)} rules={[{ required: true }]} label={t('model')} name={['embedding', 'model']}>
<Input className="mb-5 h-12" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item<IArguments>
tooltip={t(`The_size_of_the_data_chunks`)}
rules={[{ required: true }]}
label={t('chunk_size')}
name={['embedding', 'chunk_size']}
>
<Input className="mb-5 h-12" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item<IArguments>
tooltip={t(`The_amount_of_overlap`)}
rules={[{ required: true }]}
label={t('chunk_overlap')}
name={['embedding', 'chunk_overlap']}
>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_description')} />
</Form.Item>
</Col>
</Row>
);
};
const renderPromptForm = () => {
return (
<>
<Form.Item<IArguments> tooltip={t(`A_contextual_parameter`)} label={t('scene')} name={['prompt', 'scene']}>
<TextArea rows={4} className="mb-2" />
</Form.Item>
<Form.Item<IArguments> tooltip={t(`structure_or_format`)} label={t('template')} name={['prompt', 'template']}>
<TextArea rows={7} className="mb-2" />
</Form.Item>
<Form.Item<IArguments> tooltip={t(`The_maximum_number_of_tokens`)} label={t('max_token')} name={['prompt', 'max_token']}>
<Input className="mb-2" />
</Form.Item>
</>
);
};
const renderSummary = () => {
return (
<>
<Form.Item<IArguments> rules={[{ required: true }]} label={t('max_iteration')} name={['summary', 'max_iteration']}>
<Input className="mb-2" />
</Form.Item>
<Form.Item<IArguments> rules={[{ required: true }]} label={t('concurrency_limit')} name={['summary', 'concurrency_limit']}>
<Input className="mb-2" />
</Form.Item>
</>
);
};
const items = [
{
key: 'Embedding',
label: (
<div>
<FileSearchOutlined />
{t('Embedding')}
</div>
),
children: renderEmbeddingForm(),
},
{
key: 'Prompt',
label: (
<div>
<AlertFilled />
{t('Prompt')}
</div>
),
children: renderPromptForm(),
},
{
key: 'Summary',
label: (
<div>
<BookOutlined />
{t('Summary')}
</div>
),
children: renderSummary(),
},
];
const handleSubmit = async (fieldsValue: IArguments) => {
setSpinning(true);
const [_, data, res] = await apiInterceptors(
saveArguments(space.name, {
argument: JSON.stringify(fieldsValue),
}),
);
setSpinning(false);
res?.success && setArgumentsShow(false);
};
return (
<Modal
width={850}
open={argumentsShow}
onCancel={() => {
setArgumentsShow(false);
}}
footer={null}
>
<Spin spinning={spinning}>
<Form
size="large"
className="mt-4"
layout="vertical"
name="basic"
initialValues={{ ...newSpaceArguments }}
autoComplete="off"
onFinish={handleSubmit}
>
<Tabs items={items}></Tabs>
<div className="mt-3 mb-3">
<Button htmlType="submit" type="primary" className="mr-6">
{t('Submit')}
</Button>
<Button
onClick={() => {
setArgumentsShow(false);
}}
>
{t('close')}
</Button>
</div>
</Form>
</Spin>
</Modal>
);
}

View File

@@ -0,0 +1,11 @@
import { FileTextFilled, FileWordTwoTone, IeCircleFilled } from '@ant-design/icons';
export default function DocIcon({ type }: { type: string }) {
if (type === 'TEXT') {
return <FileTextFilled className="text-[#2AA3FF] mr-2 !text-lg" />;
} else if (type === 'DOCUMENT') {
return <FileWordTwoTone className="text-[#2AA3FF] mr-2 !text-lg" />;
} else {
return <IeCircleFilled className="text-[#2AA3FF] mr-2 !text-lg" />;
}
}

View File

@@ -0,0 +1,219 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Button, Card, Space, Divider, Empty, Spin, Tag, Tooltip, Modal } from 'antd';
import { DeleteFilled, InteractionFilled, PlusOutlined, ToolFilled, EyeFilled, WarningOutlined } from '@ant-design/icons';
import { apiInterceptors, delDocument, getDocumentList, syncDocument } from '@/client/api';
import { IDocument, ISpace } from '@/types/knowledge';
import moment from 'moment';
import ArgumentsModal from './arguments-modal';
import { useTranslation } from 'react-i18next';
import { useRouter } from 'next/router';
import DocIcon from './doc-icon';
interface IProps {
space: ISpace;
onAddDoc: (spaceName: string) => void;
onDeleteDoc: () => void;
}
const { confirm } = Modal;
export default function DocPanel(props: IProps) {
const { space } = props;
const { t } = useTranslation();
const router = useRouter();
const page_size = 18;
const [isLoading, setIsLoading] = useState<boolean>(false);
const [documents, setDocuments] = useState<any>([]);
const [argumentsShow, setArgumentsShow] = useState<boolean>(false);
const [total, setTotal] = useState<number>(0);
const currentPageRef = useRef(1);
const hasMore = useMemo(() => {
return documents.length < total;
}, [documents.length, total]);
const showDeleteConfirm = (row: any) => {
confirm({
title: t('Tips'),
icon: <WarningOutlined />,
content: `${t('Del_Document_Tips')}?`,
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
await handleDelete(row);
},
});
};
async function fetchDocuments() {
setIsLoading(true);
const [_, data] = await apiInterceptors(
getDocumentList(space.name, {
page: currentPageRef.current,
page_size,
}),
);
setDocuments(data?.data);
setTotal(data?.total);
setIsLoading(false);
}
const loadMoreDocuments = async () => {
if (!hasMore) {
return;
}
setIsLoading(true);
currentPageRef.current += 1;
const [_, data] = await apiInterceptors(
getDocumentList(space.name, {
page: currentPageRef.current,
page_size,
}),
);
setDocuments([...documents, ...data!.data]);
setIsLoading(false);
};
const handleSync = async (spaceName: string, id: number) => {
await apiInterceptors(syncDocument(spaceName, { doc_ids: [id] }));
};
const handleDelete = async (row: any) => {
await apiInterceptors(delDocument(space.name, { doc_name: row.doc_name }));
fetchDocuments();
props.onDeleteDoc();
};
const handleAddDocument = () => {
props.onAddDoc(space.name);
};
const handleArguments = () => {
setArgumentsShow(true);
};
const renderResultTag = (status: string, result: string) => {
let color;
switch (status) {
case 'TODO':
color = 'gold';
break;
case 'RUNNING':
color = '#2db7f5';
break;
case 'FINISHED':
color = '#87d068';
break;
case 'FAILED':
color = 'f50';
break;
default:
color = 'f50';
break;
}
return (
<Tooltip title={result}>
<Tag color={color}>{status}</Tag>
</Tooltip>
);
};
useEffect(() => {
fetchDocuments();
}, [space]);
const renderDocumentCard = () => {
if (documents?.length > 0) {
return (
<div className="max-h-96 overflow-auto max-w-3/4">
<div className="mt-3 grid grid-cols-1 gap-x-6 gap-y-5 sm:grid-cols-2 lg:grid-cols-3 xl:gap-x-5">
{documents.map((document: IDocument) => {
return (
<Card
key={document.id}
className=" dark:bg-[#484848] relative shrink-0 grow-0 cursor-pointer rounded-[10px] border border-gray-200 border-solid w-full"
title={
<Tooltip title={document.doc_name}>
<div className="truncate ">
<DocIcon type={document.doc_type} />
<span>{document.doc_name}</span>
</div>
</Tooltip>
}
extra={
<div className="mx-3">
<Tooltip title={'detail'}>
<EyeFilled
className="mr-2 !text-lg"
style={{ color: '#1b7eff', fontSize: '20px' }}
onClick={() => {
router.push(`/knowledge/chunk/?spaceName=${space.name}&id=${document.id}`);
}}
/>
</Tooltip>
<Tooltip title={'Sync'}>
<InteractionFilled
className="mr-2 !text-lg"
style={{ color: '#1b7eff', fontSize: '20px' }}
onClick={() => {
handleSync(space.name, document.id);
}}
/>
</Tooltip>
<Tooltip title={'Delete'}>
<DeleteFilled
className="text-[#ff1b2e] !text-lg"
onClick={() => {
showDeleteConfirm(document);
}}
/>
</Tooltip>
</div>
}
>
<p className="mt-2 font-semibold ">{t('Size')}:</p>
<p>{document.chunk_size} chunks</p>
<p className="mt-2 font-semibold ">{t('Last_Synch')}:</p>
<p>{moment(document.last_sync).format('YYYY-MM-DD HH:MM:SS')}</p>
<p className="mt-2 mb-2">{renderResultTag(document.status, document.result)}</p>
</Card>
);
})}
</div>
{hasMore && (
<Divider>
<span className="cursor-pointer" onClick={loadMoreDocuments}>
{t('Load_More')}
</span>
</Divider>
)}
</div>
);
}
return (
<Empty image={Empty.PRESENTED_IMAGE_DEFAULT}>
<Button type="primary" className="flex items-center mx-auto" icon={<PlusOutlined />} onClick={handleAddDocument}>
Create Now
</Button>
</Empty>
);
};
return (
<div className="collapse-container pt-2 px-4">
<Space>
<Button size="middle" type="primary" className="flex items-center" icon={<PlusOutlined />} onClick={handleAddDocument}>
{t('Add_Datasource')}
</Button>
<Button size="middle" className="flex items-center mx-2" icon={<ToolFilled />} onClick={handleArguments}>
Arguments
</Button>
</Space>
<Divider />
<Spin spinning={isLoading}>{renderDocumentCard()}</Spin>
<ArgumentsModal space={space} argumentsShow={argumentsShow} setArgumentsShow={setArgumentsShow} />
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { StepChangeParams } from '@/types/knowledge';
import { Card } from 'antd';
import { useTranslation } from 'react-i18next';
import DocIcon from './doc-icon';
type IProps = {
handleStepChange: (params: StepChangeParams) => void;
};
export default function DocTypeForm(props: IProps) {
const { t } = useTranslation();
const { handleStepChange } = props;
const docTypeList = [
{
type: 'text',
title: t('Text'),
subTitle: t('Fill your raw text'),
iconType: 'TEXT',
},
{
type: 'webPage',
title: t('URL'),
subTitle: t('Fetch_the_content_of_a_URL'),
iconType: 'WEBPAGE',
},
{
type: 'file',
title: t('Document'),
subTitle: t('Upload_a_document'),
iconType: 'DOCUMENT',
},
];
return (
<>
{docTypeList.map((type, index) => (
<Card
key={index}
className="mt-4 mb-4 cursor-pointer"
onClick={() => {
handleStepChange({ label: 'forward', docType: type.type });
}}
>
<div className="font-semibold">
<DocIcon type={type.iconType} />
{type.title}
</div>
<div>{type.subTitle}</div>
</Card>
))}
</>
);
}

View File

@@ -0,0 +1,190 @@
import { Button, Form, Input, Switch, Upload, message, Spin } from 'antd';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InboxOutlined } from '@ant-design/icons';
import { apiInterceptors, addDocument, uploadDocument, syncDocument } from '@/client/api';
import { RcFile, UploadChangeParam } from 'antd/es/upload';
import { StepChangeParams } from '@/types/knowledge';
type FileParams = {
file: RcFile;
fileList: FileList;
};
type IProps = {
handleStepChange: (params: StepChangeParams) => void;
spaceName: string;
docType: string;
};
type FieldType = {
synchChecked: boolean;
docName: string;
textSource: string;
originFileObj: FileParams;
text: string;
webPageUrl: string;
};
const { Dragger } = Upload;
const { TextArea } = Input;
export default function DocUploadForm(props: IProps) {
const { handleStepChange, spaceName, docType } = props;
const { t } = useTranslation();
const [form] = Form.useForm();
const [spinning, setSpinning] = useState<boolean>(false);
const handleFinish = async (data: FieldType) => {
const { synchChecked, docName, textSource, originFileObj, text, webPageUrl } = data;
let res;
setSpinning(true);
switch (docType) {
case 'webPage':
res = await apiInterceptors(
addDocument(spaceName as string, {
doc_name: docName,
content: webPageUrl,
doc_type: 'URL',
}),
);
break;
case 'file':
const formData = new FormData();
formData.append('doc_name', docName || originFileObj.file.name);
formData.append('doc_file', originFileObj.file);
formData.append('doc_type', 'DOCUMENT');
res = await apiInterceptors(uploadDocument(spaceName as string, formData));
break;
default:
res = await apiInterceptors(
addDocument(spaceName as string, {
doc_name: docName,
source: textSource,
content: text,
doc_type: 'TEXT',
}),
);
break;
}
synchChecked && handleSync?.(spaceName as string, res?.[1] as number);
setSpinning(false);
handleStepChange({ label: 'finish' });
};
const handleSync = async (knowledgeName: string, id: number) => {
await apiInterceptors(syncDocument(knowledgeName, { doc_ids: [id] }));
};
const handleFileChange = ({ file, fileList }: UploadChangeParam) => {
if (!form.getFieldsValue().docName) {
form.setFieldValue('docName', file.name);
}
if (fileList.length === 0) {
form.setFieldValue('originFileObj', null);
}
};
const beforeUpload = () => {
const curFile = form.getFieldsValue().originFileObj;
if (!curFile) {
return false;
}
message.warning(t('Limit_Upload_File_Count_Tips'));
return Upload.LIST_IGNORE;
};
const renderChooseType = () => {};
const renderText = () => {
return (
<>
<Form.Item<FieldType>
label={`${t('Text_Source')}:`}
name="textSource"
rules={[{ required: true, message: t('Please_input_the_text_source') }]}
>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_text_source')} />
</Form.Item>
<Form.Item<FieldType> label={`${t('Text')}:`} name="text" rules={[{ required: true, message: t('Please_input_the_description') }]}>
<TextArea rows={4} />
</Form.Item>
</>
);
};
const renderWebPage = () => {
return (
<>
<Form.Item<FieldType> label={`${t('Web_Page_URL')}:`} name="webPageUrl" rules={[{ required: true, message: t('Please_input_the_owner') }]}>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_Web_Page_URL')} />
</Form.Item>
</>
);
};
const renderDocument = () => {
return (
<>
<Form.Item<FieldType> name="originFileObj" rules={[{ required: true, message: t('Please_input_the_owner') }]}>
<Dragger onChange={handleFileChange} beforeUpload={beforeUpload} multiple={false} accept=".pdf,.ppt,.pptx,.xls,.xlsx,.doc,.docx,.txt,.md">
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p style={{ color: 'rgb(22, 108, 255)', fontSize: '20px' }}>{t('Select_or_Drop_file')}</p>
<p className="ant-upload-hint" style={{ color: 'rgb(22, 108, 255)' }}>
PDF, PowerPoint, Excel, Word, Text, Markdown,
</p>
</Dragger>
</Form.Item>
</>
);
};
const renderFormContainer = () => {
switch (docType) {
case 'webPage':
return renderWebPage();
case 'file':
return renderDocument();
default:
return renderText();
}
};
return (
<Spin spinning={spinning}>
<Form
form={form}
size="large"
className="mt-4"
layout="vertical"
name="basic"
initialValues={{ remember: true }}
autoComplete="off"
onFinish={handleFinish}
>
<Form.Item<FieldType> label={`${t('Name')}:`} name="docName" rules={[{ required: true, message: t('Please_input_the_name') }]}>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_name')} />
</Form.Item>
{renderFormContainer()}
<Form.Item<FieldType> label={`${t('Synch')}:`} name="synchChecked" initialValue={true}>
<Switch className="bg-slate-400" defaultChecked />
</Form.Item>
<Form.Item>
<Button
onClick={() => {
handleStepChange({ label: 'back' });
}}
className="mr-4"
>{`${t('Back')}`}</Button>
<Button type="primary" htmlType="submit">
{t('Finish')}
</Button>
</Form.Item>
</Form>
</Spin>
);
}

View File

@@ -0,0 +1,117 @@
import { Popover, ConfigProvider, Button, Modal, Badge } from 'antd';
import { useRouter } from 'next/router';
import Image from 'next/image';
import { DeleteTwoTone, MessageTwoTone, WarningOutlined } from '@ant-design/icons';
import { ISpace } from '@/types/knowledge';
import DocPanel from './doc-panel';
import moment from 'moment';
import { apiInterceptors, delSpace, newDialogue } from '@/client/api';
import { useTranslation } from 'react-i18next';
import { VECTOR_ICON_MAP } from '@/utils/constants';
interface IProps {
space: ISpace;
onAddDoc: (spaceName: string) => void;
getSpaces: () => void;
}
const { confirm } = Modal;
export default function SpaceCard(props: IProps) {
const router = useRouter();
const { t } = useTranslation();
const { space, getSpaces } = props;
const showDeleteConfirm = () => {
confirm({
title: t('Tips'),
icon: <WarningOutlined />,
content: `${t('Del_Knowledge_Tips')}?`,
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
await apiInterceptors(delSpace({ name: space?.name }));
getSpaces();
},
});
};
function onDeleteDoc() {
getSpaces();
}
const handleChat = async (e: any) => {
e.stopPropagation();
const [_, data] = await apiInterceptors(
newDialogue({
chat_mode: 'chat_knowledge',
}),
);
if (data?.conv_uid) {
router.push(`/chat?scene=chat_knowledge&id=${data?.conv_uid}&db_param=${space.name}`);
}
};
const renderVectorIcon = (type: string) => {
return (
<Image
className="rounded-full w-8 h-8 border border-gray-200 object-contain bg-white inline-block"
width={36}
height={136}
src={VECTOR_ICON_MAP[type] || '/models/knowledge-default.jpg'}
alt="llm"
/>
);
};
return (
<ConfigProvider
theme={{
components: {
Popover: {
zIndexPopup: 90,
},
},
}}
>
<Popover
className="dark:hover:border-white transition-all hover:shadow-md bg-[#FFFFFF] dark:bg-[#484848] cursor-pointer rounded-[10px] border border-gray-200 border-solid"
placement="bottom"
trigger="click"
content={<DocPanel space={space} onAddDoc={props.onAddDoc} onDeleteDoc={onDeleteDoc} />}
>
<Badge className="mr-4 mb-4 min-w-[200px] sm:w-60 lg:w-72" count={space.docs || 0}>
<div className="flex justify-between mx-6 mt-3">
<div className="text-lg font-bold text-black truncate">
{renderVectorIcon(space.vector_type)}
<span className="dark:text-white ml-2">{space?.name}</span>
</div>
<DeleteTwoTone
onClick={(e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
showDeleteConfirm();
}}
twoToneColor="#CD2029"
className="!text-2xl"
/>
</div>
<div className="text-sm mt-2 p-6 pt-2 h-40">
<p className="font-semibold">{t('Owner')}:</p>
<p className=" truncate">{space?.owner}</p>
<p className="font-semibold mt-2">{t('Description')}:</p>
<p className=" line-clamp-2">{space?.desc}</p>
<p className="font-semibold mt-2">Last modify:</p>
<p className=" truncate">{moment(space.gmt_modified).format('YYYY-MM-DD HH:MM:SS')}</p>
</div>
<div className="flex justify-center">
<Button size="middle" onClick={handleChat} className="mr-4 dark:text-white mb-2" shape="round" icon={<MessageTwoTone />}>
{t('Chat')}
</Button>
</div>
</Badge>
</Popover>
</ConfigProvider>
);
}

View File

@@ -0,0 +1,78 @@
import { addSpace, apiInterceptors } from '@/client/api';
import { StepChangeParams } from '@/types/knowledge';
import { Button, Form, Input, Spin } from 'antd';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
type FieldType = {
spaceName: string;
owner: string;
description: string;
};
type IProps = {
handleStepChange: (params: StepChangeParams) => void;
};
export default function SpaceForm(props: IProps) {
const { t } = useTranslation();
const { handleStepChange } = props;
const [spinning, setSpinning] = useState<boolean>(false);
const handleFinish = async (fieldsValue: FieldType) => {
const { spaceName, owner, description } = fieldsValue;
setSpinning(true);
const [_, data, res] = await apiInterceptors(
addSpace({
name: spaceName,
vector_type: 'Chroma',
owner,
desc: description,
}),
);
setSpinning(false);
res?.success && handleStepChange({ label: 'forward', spaceName });
};
return (
<Spin spinning={spinning}>
<Form
size="large"
className="mt-4"
layout="vertical"
name="basic"
initialValues={{ remember: true }}
autoComplete="off"
onFinish={handleFinish}
>
<Form.Item<FieldType>
label={t('Knowledge_Space_Name')}
name="spaceName"
rules={[
{ required: true, message: t('Please_input_the_name') },
() => ({
validator(_, value) {
if (/[^\u4e00-\u9fa50-9a-zA-Z_-]/.test(value)) {
return Promise.reject(new Error(t('the_name_can_only_contain')));
}
return Promise.resolve();
},
}),
]}
>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_name')} />
</Form.Item>
<Form.Item<FieldType> label={t('Owner')} name="owner" rules={[{ required: true, message: t('Please_input_the_owner') }]}>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_owner')} />
</Form.Item>
<Form.Item<FieldType> label={t('Description')} name="description" rules={[{ required: true, message: t('Please_input_the_description') }]}>
<Input className="mb-5 h-12" placeholder={t('Please_input_the_description')} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{t('Next')}
</Button>
</Form.Item>
</Form>
</Spin>
);
}