mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-10 05:19:44 +00:00
refactor: Add frontend code to DB-GPT (#912)
This commit is contained in:
192
web/components/knowledge/arguments-modal.tsx
Normal file
192
web/components/knowledge/arguments-modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
11
web/components/knowledge/doc-icon.tsx
Normal file
11
web/components/knowledge/doc-icon.tsx
Normal 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" />;
|
||||
}
|
||||
}
|
219
web/components/knowledge/doc-panel.tsx
Normal file
219
web/components/knowledge/doc-panel.tsx
Normal 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>
|
||||
);
|
||||
}
|
53
web/components/knowledge/doc-type-form.tsx
Normal file
53
web/components/knowledge/doc-type-form.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
190
web/components/knowledge/doc-upload-form.tsx
Normal file
190
web/components/knowledge/doc-upload-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
117
web/components/knowledge/space-card.tsx
Normal file
117
web/components/knowledge/space-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
78
web/components/knowledge/space-form.tsx
Normal file
78
web/components/knowledge/space-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user