chore: import upstream snapshot with attribution
Update draft releases / main (push) Has been cancelled
Build and push docs image / build-image (push) Has been cancelled
Build Web Application / build-web (macos-latest) (push) Has been cancelled
Build Web Application / build-web (ubuntu-latest) (push) Has been cancelled
Python Code Quality Checks / build (push) Has been cancelled
Test Python / test-python (macos-latest, 3.10) (push) Has been cancelled
Test Python / test-python (macos-latest, 3.11) (push) Has been cancelled
Test Python / test-python (ubuntu-latest, 3.10) (push) Has been cancelled
Test Python / test-python (ubuntu-latest, 3.11) (push) Has been cancelled
Update draft releases / main (push) Has been cancelled
Build and push docs image / build-image (push) Has been cancelled
Build Web Application / build-web (macos-latest) (push) Has been cancelled
Build Web Application / build-web (ubuntu-latest) (push) Has been cancelled
Python Code Quality Checks / build (push) Has been cancelled
Test Python / test-python (macos-latest, 3.10) (push) Has been cancelled
Test Python / test-python (macos-latest, 3.11) (push) Has been cancelled
Test Python / test-python (ubuntu-latest, 3.10) (push) Has been cancelled
Test Python / test-python (ubuntu-latest, 3.11) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
import { apiInterceptors, chunkAddQuestion, getChunkList } from '@/client/api';
|
||||
import MenuModal from '@/components/MenuModal';
|
||||
import MarkDownContext from '@/new-components/common/MarkdownContext';
|
||||
import { MinusCircleOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { useRequest } from 'ahooks';
|
||||
import { App, Breadcrumb, Button, Card, Empty, Form, Input, Pagination, Space, Spin, Tag } from 'antd';
|
||||
import cls from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const DEDAULT_PAGE_SIZE = 10;
|
||||
|
||||
function ChunkList() {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [chunkList, setChunkList] = useState<any>([]);
|
||||
const [total, setTotal] = useState<number>(0);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
// const [isExpand, setIsExpand] = useState<boolean>(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [currentChunkInfo, setCurrentChunkInfo] = useState<any>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { message } = App.useApp();
|
||||
|
||||
const {
|
||||
query: { id, spaceName },
|
||||
} = useRouter();
|
||||
|
||||
const fetchChunks = async () => {
|
||||
setLoading(true);
|
||||
const [_, data] = await apiInterceptors(
|
||||
getChunkList(spaceName as string, {
|
||||
document_id: id as string,
|
||||
page: 1,
|
||||
page_size: DEDAULT_PAGE_SIZE,
|
||||
}),
|
||||
);
|
||||
setChunkList(data?.data);
|
||||
setTotal(data?.total ?? 0);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const loaderMore = async (page: number, page_size: number) => {
|
||||
setPageSize(page_size);
|
||||
setLoading(true);
|
||||
const [_, data] = await apiInterceptors(
|
||||
getChunkList(spaceName as string, {
|
||||
document_id: id as string,
|
||||
page,
|
||||
page_size,
|
||||
}),
|
||||
);
|
||||
setChunkList(data?.data || []);
|
||||
setLoading(false);
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
spaceName && id && fetchChunks();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id, spaceName]);
|
||||
|
||||
const onSearch = async (e: any) => {
|
||||
const content = e.target.value;
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
const [_, data] = await apiInterceptors(
|
||||
getChunkList(spaceName as string, {
|
||||
document_id: id as string,
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
content,
|
||||
}),
|
||||
);
|
||||
setChunkList(data?.data || []);
|
||||
};
|
||||
|
||||
// 添加问题
|
||||
const { run: addQuestionRun, loading: addLoading } = useRequest(
|
||||
async (questions: string[]) => apiInterceptors(chunkAddQuestion({ chunk_id: currentChunkInfo.id, questions })),
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: async () => {
|
||||
message.success('添加成功');
|
||||
setIsModalOpen(false);
|
||||
await fetchChunks();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full w-full px-6 pb-6'>
|
||||
<Breadcrumb
|
||||
className='m-6'
|
||||
items={[
|
||||
{
|
||||
title: 'Knowledge',
|
||||
onClick() {
|
||||
router.back();
|
||||
},
|
||||
path: '/knowledge',
|
||||
},
|
||||
{
|
||||
title: spaceName,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Input
|
||||
className='w-1/5 h-10 mb-4'
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder={t('please_enter_the_keywords')}
|
||||
onChange={debounce(onSearch, 300)}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
{chunkList?.length > 0 ? (
|
||||
<div className='h-full grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 grid-flow-row auto-rows-max gap-x-6 gap-y-10 overflow-y-auto relative'>
|
||||
<Spin
|
||||
className='flex flex-col items-center justify-center absolute bottom-0 top-0 left-0 right-0'
|
||||
spinning={loading}
|
||||
/>
|
||||
{chunkList?.map((chunk: any, index: number) => {
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
key={chunk.id}
|
||||
title={
|
||||
<Space className='flex justify-between'>
|
||||
<Tag color='blue'># {index + (currentPage - 1) * DEDAULT_PAGE_SIZE}</Tag>
|
||||
{/* <DocIcon type={chunk.doc_type} /> */}
|
||||
<span className='text-sm'>{chunk.doc_name}</span>
|
||||
</Space>
|
||||
}
|
||||
className={cls('h-96 rounded-xl overflow-hidden', {
|
||||
// 'h-auto': isExpand,
|
||||
'h-auto': true,
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
setCurrentChunkInfo(chunk);
|
||||
}}
|
||||
>
|
||||
<p className='font-semibold'>{t('Content')}:</p>
|
||||
<p>{chunk?.content}</p>
|
||||
<p className='font-semibold'>{t('Meta_Data')}: </p>
|
||||
<p>{chunk?.meta_info}</p>
|
||||
{/* <Space
|
||||
className="absolute bottom-0 right-0 left-0 flex items-center justify-center cursor-pointer text-[#1890ff] bg-[rgba(255,255,255,0.8)] z-30"
|
||||
onClick={() => setIsExpand(!isExpand)}
|
||||
>
|
||||
<DoubleRightOutlined rotate={isExpand ? -90 : 90} /> {isExpand ? '收起' : '展开'}
|
||||
</Space> */}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Spin spinning={loading}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_DEFAULT} />
|
||||
</Spin>
|
||||
)}
|
||||
<Pagination
|
||||
className='flex w-full justify-end'
|
||||
defaultCurrent={1}
|
||||
defaultPageSize={DEDAULT_PAGE_SIZE}
|
||||
total={total}
|
||||
showTotal={total => `Total ${total} items`}
|
||||
onChange={loaderMore}
|
||||
/>
|
||||
<MenuModal
|
||||
modal={{
|
||||
title: t('Manual_entry'),
|
||||
width: '70%',
|
||||
open: isModalOpen,
|
||||
footer: false,
|
||||
onCancel: () => setIsModalOpen(false),
|
||||
afterOpenChange: open => {
|
||||
if (open) {
|
||||
form.setFieldValue(
|
||||
'questions',
|
||||
JSON.parse(currentChunkInfo?.questions || '[]')?.map((item: any) => ({ question: item })),
|
||||
);
|
||||
}
|
||||
},
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
key: 'edit',
|
||||
label: t('Data_content'),
|
||||
children: (
|
||||
<div className='flex gap-4'>
|
||||
<Card size='small' title={t('Main_content')} className='w-2/3 flex-wrap overflow-y-auto'>
|
||||
<MarkDownContext>{currentChunkInfo?.content}</MarkDownContext>
|
||||
</Card>
|
||||
<Card size='small' title={t('Auxiliary_data')} className='w-1/3'>
|
||||
<MarkDownContext>{currentChunkInfo?.meta_info}</MarkDownContext>
|
||||
</Card>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: t('Add_problem'),
|
||||
children: (
|
||||
<Card
|
||||
size='small'
|
||||
extra={
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
onClick={async () => {
|
||||
const formVal = form.getFieldsValue();
|
||||
if (!formVal.questions) {
|
||||
message.warning(t('enter_question_first'));
|
||||
return;
|
||||
}
|
||||
if (formVal.questions?.filter(Boolean).length === 0) {
|
||||
message.warning(t('enter_question_first'));
|
||||
return;
|
||||
}
|
||||
const questions = formVal.questions?.filter(Boolean).map((item: any) => item.question);
|
||||
await addQuestionRun(questions);
|
||||
}}
|
||||
loading={addLoading}
|
||||
>
|
||||
{t('save')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form form={form}>
|
||||
<Form.List name='questions'>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name }) => (
|
||||
<div key={key} className={cls('flex flex-1 items-center gap-8')}>
|
||||
<Form.Item label='' name={[name, 'question']} className='grow'>
|
||||
<Input placeholder={t('Please_Input')} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<MinusCircleOutlined
|
||||
onClick={() => {
|
||||
remove(name);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
))}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type='dashed'
|
||||
onClick={() => {
|
||||
add();
|
||||
}}
|
||||
block
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{t('Add_problem')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Card>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChunkList;
|
||||
@@ -0,0 +1,288 @@
|
||||
import { ChatContext } from '@/app/chat-context';
|
||||
import { apiInterceptors, delSpace, getSpaceConfig, getSpaceList, newDialogue } from '@/client/api';
|
||||
import DocPanel from '@/components/knowledge/doc-panel';
|
||||
import DocTypeForm from '@/components/knowledge/doc-type-form';
|
||||
import DocUploadForm from '@/components/knowledge/doc-upload-form';
|
||||
import Segmentation from '@/components/knowledge/segmentation';
|
||||
import SpaceForm from '@/components/knowledge/space-form';
|
||||
import BlurredCard, { ChatButton, InnerDropdown } from '@/new-components/common/blurredCard';
|
||||
import ConstructLayout from '@/new-components/layout/Construct';
|
||||
import { File, ISpace, IStorage, StepChangeParams } from '@/types/knowledge';
|
||||
import { PlusOutlined, ReadOutlined, SearchOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Modal, Spin, Steps, Tag } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Knowledge = () => {
|
||||
const { setCurrentDialogInfo } = useContext(ChatContext);
|
||||
const [spaceList, setSpaceList] = useState<Array<ISpace> | null>([]);
|
||||
const [isAddShow, setIsAddShow] = useState<boolean>(false);
|
||||
const [isPanelShow, setIsPanelShow] = useState<boolean>(false);
|
||||
const [currentSpace, setCurrentSpace] = useState<ISpace>();
|
||||
|
||||
const [activeStep, setActiveStep] = useState<number>(0);
|
||||
const [spaceName, setSpaceName] = useState<string>('');
|
||||
const [files, setFiles] = useState<Array<File>>([]);
|
||||
const [docType, setDocType] = useState<string>('');
|
||||
const [addStatus, setAddStatus] = useState<string>('');
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [spaceConfig, setSpaceConfig] = useState<IStorage | null>(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const addKnowledgeSteps = [
|
||||
{ title: t('Knowledge_Space_Config') },
|
||||
{ title: t('Choose_a_Datasource_type') },
|
||||
{ title: t('Upload') },
|
||||
{ title: t('Segmentation') },
|
||||
];
|
||||
const router = useRouter();
|
||||
|
||||
async function getSpaces(params?: any) {
|
||||
setLoading(true);
|
||||
const [_, data] = await apiInterceptors(getSpaceList({ ...params }));
|
||||
setLoading(false);
|
||||
setSpaceList(data);
|
||||
}
|
||||
|
||||
async function getSpaceConfigs() {
|
||||
const [_, data] = await apiInterceptors(getSpaceConfig());
|
||||
if (!data) return null;
|
||||
setSpaceConfig(data.storage);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getSpaces();
|
||||
getSpaceConfigs();
|
||||
}, []);
|
||||
|
||||
const handleChat = async (space: ISpace) => {
|
||||
const [_, data] = await apiInterceptors(
|
||||
newDialogue({
|
||||
chat_mode: 'chat_knowledge',
|
||||
}),
|
||||
);
|
||||
// 知识库对话都默认私有知识库应用下
|
||||
if (data?.conv_uid) {
|
||||
setCurrentDialogInfo?.({
|
||||
chat_scene: data.chat_mode,
|
||||
app_code: data.chat_mode,
|
||||
});
|
||||
localStorage.setItem(
|
||||
'cur_dialog_info',
|
||||
JSON.stringify({
|
||||
chat_scene: data.chat_mode,
|
||||
app_code: data.chat_mode,
|
||||
}),
|
||||
);
|
||||
router.push(`/chat?scene=chat_knowledge&id=${data?.conv_uid}&knowledge_id=${space.name}`);
|
||||
}
|
||||
};
|
||||
const handleStepChange = ({ label, spaceName, docType, files }: StepChangeParams) => {
|
||||
if (label === 'finish') {
|
||||
setIsAddShow(false);
|
||||
getSpaces();
|
||||
setSpaceName('');
|
||||
setDocType('');
|
||||
setAddStatus('finish');
|
||||
localStorage.removeItem('cur_space_id');
|
||||
} else if (label === 'forward') {
|
||||
activeStep === 0 && getSpaces();
|
||||
setActiveStep(step => step + 1);
|
||||
} else {
|
||||
setActiveStep(step => step - 1);
|
||||
}
|
||||
files && setFiles(files);
|
||||
spaceName && setSpaceName(spaceName);
|
||||
docType && setDocType(docType);
|
||||
};
|
||||
|
||||
function onAddDoc(spaceName: string) {
|
||||
setSpaceName(spaceName);
|
||||
setActiveStep(1);
|
||||
setIsAddShow(true);
|
||||
setAddStatus('start');
|
||||
}
|
||||
const showDeleteConfirm = (space: ISpace) => {
|
||||
Modal.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();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onSearch = async (e: any) => {
|
||||
getSpaces({ name: e.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<ConstructLayout>
|
||||
<Spin spinning={loading}>
|
||||
<div className='page-body p-4 md:p-6 h-[90vh] overflow-auto'>
|
||||
{/* <Button
|
||||
type="primary"
|
||||
className="flex items-center"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setIsAddShow(true);
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button> */}
|
||||
<div className='flex justify-between items-center mb-6'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<Input
|
||||
variant='filled'
|
||||
prefix={<SearchOutlined />}
|
||||
placeholder={t('please_enter_the_keywords')}
|
||||
onChange={debounce(onSearch, 300)}
|
||||
allowClear
|
||||
className='w-[230px] h-[40px] border-1 border-white backdrop-filter backdrop-blur-lg bg-white bg-opacity-30 dark:border-[#6f7f95] dark:bg-[#6f7f95] dark:bg-opacity-60'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-4'>
|
||||
<Button
|
||||
className='border-none text-white bg-button-gradient'
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setIsAddShow(true);
|
||||
}}
|
||||
>
|
||||
{t('create_knowledge')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-wrap mt-4 mx-[-8px]'>
|
||||
{spaceList?.map((space: ISpace) => (
|
||||
<BlurredCard
|
||||
onClick={() => {
|
||||
setCurrentSpace(space);
|
||||
setIsPanelShow(true);
|
||||
localStorage.setItem('cur_space_id', JSON.stringify(space.id));
|
||||
}}
|
||||
description={space.desc}
|
||||
name={space.name}
|
||||
key={space.id}
|
||||
logo={
|
||||
space.domain_type === 'FinancialReport'
|
||||
? '/models/fin_report.jpg'
|
||||
: space.vector_type === 'KnowledgeGraph'
|
||||
? '/models/knowledge-graph.png'
|
||||
: space.vector_type === 'FullText'
|
||||
? '/models/knowledge-full-text.jpg'
|
||||
: '/models/knowledge-default.jpg'
|
||||
}
|
||||
RightTop={
|
||||
<InnerDropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'del',
|
||||
label: (
|
||||
<span className='text-red-400' onClick={() => showDeleteConfirm(space)}>
|
||||
{t('Delete')}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
}
|
||||
rightTopHover={false}
|
||||
Tags={
|
||||
<div className='flex item-center'>
|
||||
<Tag>
|
||||
<span className='flex items-center gap-1'>
|
||||
<ReadOutlined className='mt-[1px]' />
|
||||
{space.docs}
|
||||
</span>
|
||||
</Tag>
|
||||
<Tag>
|
||||
<span className='flex items-center gap-1'>{space.domain_type || 'Normal'}</span>
|
||||
</Tag>
|
||||
{space.vector_type ? (
|
||||
<Tag>
|
||||
<span className='flex items-center gap-1'>{space.vector_type}</span>
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
}
|
||||
LeftBottom={
|
||||
<div className='flex gap-2'>
|
||||
<span>{space.owner}</span>
|
||||
<span>•</span>
|
||||
{space?.gmt_modified && <span>{moment(space?.gmt_modified).fromNow() + ' ' + t('update')}</span>}
|
||||
</div>
|
||||
}
|
||||
RightBottom={
|
||||
<ChatButton
|
||||
text={t('start_chat')}
|
||||
onClick={() => {
|
||||
handleChat(space);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
className='h-5/6 overflow-hidden'
|
||||
open={isPanelShow}
|
||||
width={'70%'}
|
||||
onCancel={() => setIsPanelShow(false)}
|
||||
footer={null}
|
||||
destroyOnClose={true}
|
||||
>
|
||||
<DocPanel space={currentSpace!} onAddDoc={onAddDoc} onDeleteDoc={getSpaces} addStatus={addStatus} />
|
||||
</Modal>
|
||||
<Modal
|
||||
title={t('New_knowledge_base')}
|
||||
centered
|
||||
open={isAddShow}
|
||||
destroyOnClose={true}
|
||||
onCancel={() => {
|
||||
setIsAddShow(false);
|
||||
}}
|
||||
width={1000}
|
||||
afterClose={() => {
|
||||
setActiveStep(0);
|
||||
getSpaces();
|
||||
}}
|
||||
footer={null}
|
||||
>
|
||||
<Steps current={activeStep} items={addKnowledgeSteps} />
|
||||
{activeStep === 0 && <SpaceForm handleStepChange={handleStepChange} spaceConfig={spaceConfig} />}
|
||||
{activeStep === 1 && <DocTypeForm handleStepChange={handleStepChange} />}
|
||||
<DocUploadForm
|
||||
className={classNames({ hidden: activeStep !== 2 })}
|
||||
spaceName={spaceName}
|
||||
docType={docType}
|
||||
handleStepChange={handleStepChange}
|
||||
/>
|
||||
{activeStep === 3 && (
|
||||
<Segmentation
|
||||
spaceName={spaceName}
|
||||
docType={docType}
|
||||
uploadFiles={files}
|
||||
handleStepChange={handleStepChange}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</Spin>
|
||||
</ConstructLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Knowledge;
|
||||
Reference in New Issue
Block a user