| @@ -75,3 +75,30 @@ export interface IChunk { | |||
| img_id: string; | |||
| important_kwd: any[]; | |||
| } | |||
| export interface ITestingChunk { | |||
| chunk_id: string; | |||
| content_ltks: string; | |||
| content_with_weight: string; | |||
| doc_id: string; | |||
| docnm_kwd: string; | |||
| img_id: string; | |||
| important_kwd: any[]; | |||
| kb_id: string; | |||
| similarity: number; | |||
| term_similarity: number; | |||
| vector: number[]; | |||
| vector_similarity: number; | |||
| } | |||
| export interface ITestingDocument { | |||
| count: number; | |||
| doc_id: string; | |||
| doc_name: string; | |||
| } | |||
| export interface ITestingResult { | |||
| chunks: ITestingChunk[]; | |||
| doc_aggs: Record<string, number>; | |||
| total: number; | |||
| } | |||
| @@ -1,14 +1,47 @@ | |||
| import { Flex } from 'antd'; | |||
| import { Flex, Form } from 'antd'; | |||
| import TestingControl from './testing-control'; | |||
| import TestingResult from './testing-result'; | |||
| import { useKnowledgeBaseId } from '@/hooks/knowledgeHook'; | |||
| import { useEffect } from 'react'; | |||
| import { useDispatch } from 'umi'; | |||
| import styles from './index.less'; | |||
| const KnowledgeTesting = () => { | |||
| const [form] = Form.useForm(); | |||
| const dispatch = useDispatch(); | |||
| const knowledgeBaseId = useKnowledgeBaseId(); | |||
| const handleTesting = async () => { | |||
| const values = await form.validateFields(); | |||
| console.info(values); | |||
| const similarity_threshold = values.similarity_threshold / 100; | |||
| const vector_similarity_weight = values.vector_similarity_weight / 100; | |||
| dispatch({ | |||
| type: 'testingModel/testDocumentChunk', | |||
| payload: { | |||
| ...values, | |||
| similarity_threshold, | |||
| vector_similarity_weight, | |||
| kb_id: knowledgeBaseId, | |||
| }, | |||
| }); | |||
| }; | |||
| useEffect(() => { | |||
| return () => { | |||
| dispatch({ type: 'testingModel/reset' }); | |||
| }; | |||
| }, [dispatch]); | |||
| return ( | |||
| <Flex className={styles.testingWrapper} gap={16}> | |||
| <TestingControl></TestingControl> | |||
| <TestingResult></TestingResult> | |||
| <TestingControl | |||
| form={form} | |||
| handleTesting={handleTesting} | |||
| ></TestingControl> | |||
| <TestingResult handleTesting={handleTesting}></TestingResult> | |||
| </Flex> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,72 @@ | |||
| import { BaseState } from '@/interfaces/common'; | |||
| import { | |||
| ITestingChunk, | |||
| ITestingDocument, | |||
| } from '@/interfaces/database/knowledge'; | |||
| import kbService from '@/services/kbService'; | |||
| import { DvaModel } from 'umi'; | |||
| export interface TestingModelState extends Pick<BaseState, 'pagination'> { | |||
| chunks: ITestingChunk[]; | |||
| documents: ITestingDocument[]; | |||
| total: number; | |||
| selectedDocumentIds: string[] | undefined; | |||
| } | |||
| const initialState = { | |||
| chunks: [], | |||
| documents: [], | |||
| total: 0, | |||
| pagination: { | |||
| current: 1, | |||
| pageSize: 10, | |||
| }, | |||
| selectedDocumentIds: undefined, | |||
| }; | |||
| const model: DvaModel<TestingModelState> = { | |||
| namespace: 'testingModel', | |||
| state: initialState, | |||
| reducers: { | |||
| setChunksAndDocuments(state, { payload }) { | |||
| return { | |||
| ...state, | |||
| ...payload, | |||
| }; | |||
| }, | |||
| setPagination(state, { payload }) { | |||
| return { ...state, pagination: { ...state.pagination, ...payload } }; | |||
| }, | |||
| setSelectedDocumentIds(state, { payload }) { | |||
| return { ...state, selectedDocumentIds: payload }; | |||
| }, | |||
| reset() { | |||
| return initialState; | |||
| }, | |||
| }, | |||
| effects: { | |||
| *testDocumentChunk({ payload = {} }, { call, put, select }) { | |||
| const { pagination, selectedDocumentIds }: TestingModelState = | |||
| yield select((state: any) => state.testingModel); | |||
| const { data } = yield call(kbService.retrieval_test, { | |||
| ...payload, | |||
| doc_ids: selectedDocumentIds, | |||
| page: pagination.current, | |||
| size: pagination.pageSize, | |||
| }); | |||
| const { retcode, data: res } = data; | |||
| if (retcode === 0) { | |||
| yield put({ | |||
| type: 'setChunksAndDocuments', | |||
| payload: { | |||
| chunks: res.chunks, | |||
| documents: res.doc_aggs, | |||
| total: res.total, | |||
| }, | |||
| }); | |||
| } | |||
| }, | |||
| }, | |||
| }; | |||
| export default model; | |||
| @@ -2,6 +2,9 @@ | |||
| width: 350px; | |||
| background-color: white; | |||
| padding: 30px 20px; | |||
| overflow: auto; | |||
| height: calc(100vh - 160px); | |||
| .historyTitle { | |||
| padding: 30px 0 20px; | |||
| } | |||
| @@ -3,6 +3,7 @@ import { | |||
| Card, | |||
| Divider, | |||
| Flex, | |||
| Form, | |||
| Input, | |||
| Slider, | |||
| SliderSingleProps, | |||
| @@ -11,50 +12,95 @@ import { | |||
| } from 'antd'; | |||
| import { DeleteOutlined, HistoryOutlined } from '@ant-design/icons'; | |||
| import { FormInstance } from 'antd/lib'; | |||
| import styles from './index.less'; | |||
| const list = [1, 2, 3]; | |||
| const marks: SliderSingleProps['marks'] = { | |||
| 0: '0°C', | |||
| 26: '26°C', | |||
| 37: '37°C', | |||
| 100: { | |||
| style: { | |||
| color: '#f50', | |||
| }, | |||
| label: <strong>100°C</strong>, | |||
| }, | |||
| 0: '0', | |||
| 100: '1', | |||
| }; | |||
| const TestingControl = () => { | |||
| type FieldType = { | |||
| similarity_threshold?: number; | |||
| vector_similarity_weight?: number; | |||
| top_k?: number; | |||
| question: string; | |||
| }; | |||
| const formatter = (value: number | undefined) => { | |||
| return typeof value === 'number' ? value / 100 : 0; | |||
| }; | |||
| const tooltip = { formatter }; | |||
| interface IProps { | |||
| form: FormInstance; | |||
| handleTesting: () => Promise<any>; | |||
| } | |||
| const TestingControl = ({ form, handleTesting }: IProps) => { | |||
| const question = Form.useWatch('question', { form, preserve: true }); | |||
| const buttonDisabled = | |||
| !question || (typeof question === 'string' && question.trim() === ''); | |||
| return ( | |||
| <section className={styles.testingControlWrapper}> | |||
| <p> | |||
| <b>Retrieval testing</b> | |||
| </p> | |||
| <p>xxxx</p> | |||
| <p>Final step! After success, leave the rest to Infiniflow AI.</p> | |||
| <Divider></Divider> | |||
| <section> | |||
| <Slider range marks={marks} defaultValue={[26, 37]} /> | |||
| <Slider range marks={marks} defaultValue={[26, 37]} /> | |||
| <Card | |||
| size="small" | |||
| title="Test text" | |||
| extra={ | |||
| <Button type="primary" ghost> | |||
| Semantic Search | |||
| </Button> | |||
| } | |||
| <Form | |||
| name="testing" | |||
| layout="vertical" | |||
| form={form} | |||
| initialValues={{ | |||
| similarity_threshold: 20, | |||
| vector_similarity_weight: 30, | |||
| top_k: 1024, | |||
| }} | |||
| > | |||
| <Input.TextArea autoSize={{ minRows: 8 }}></Input.TextArea> | |||
| <Flex justify={'space-between'}> | |||
| <Tag>10/200</Tag> | |||
| <Button type="primary" size="small"> | |||
| Testing | |||
| </Button> | |||
| </Flex> | |||
| </Card> | |||
| <Form.Item<FieldType> | |||
| label="Similarity threshold" | |||
| name={'similarity_threshold'} | |||
| > | |||
| <Slider marks={marks} defaultValue={0} tooltip={tooltip} /> | |||
| </Form.Item> | |||
| <Form.Item<FieldType> | |||
| label="Vector similarity weight" | |||
| name={'vector_similarity_weight'} | |||
| > | |||
| <Slider marks={marks} defaultValue={0} tooltip={tooltip} /> | |||
| </Form.Item> | |||
| <Form.Item<FieldType> label="Top k" name={'top_k'}> | |||
| <Slider marks={{ 0: 0, 2048: 2048 }} defaultValue={0} max={2048} /> | |||
| </Form.Item> | |||
| <Card size="small" title="Test text"> | |||
| <Form.Item<FieldType> | |||
| name={'question'} | |||
| rules={[ | |||
| { required: true, message: 'Please input your question!' }, | |||
| ]} | |||
| > | |||
| <Input.TextArea autoSize={{ minRows: 8 }}></Input.TextArea> | |||
| </Form.Item> | |||
| <Flex justify={'space-between'}> | |||
| <Tag>10/200</Tag> | |||
| <Button | |||
| type="primary" | |||
| size="small" | |||
| onClick={handleTesting} | |||
| disabled={buttonDisabled} | |||
| > | |||
| Testing | |||
| </Button> | |||
| </Flex> | |||
| </Card> | |||
| </Form> | |||
| </section> | |||
| <section> | |||
| <p className={styles.historyTitle}> | |||
| @@ -2,15 +2,35 @@ | |||
| flex: 1; | |||
| background-color: white; | |||
| padding: 30px 20px; | |||
| overflow: auto; | |||
| height: calc(100vh - 160px); | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| .selectFilesCollapse { | |||
| :global(.ant-collapse-header) { | |||
| padding-left: 22px; | |||
| } | |||
| margin-bottom: 32px; | |||
| overflow-y: auto; | |||
| } | |||
| .selectFilesTitle { | |||
| padding-right: 10px; | |||
| } | |||
| .similarityCircle { | |||
| width: 24px; | |||
| height: 24px; | |||
| border-radius: 50%; | |||
| background-color: rgba(244, 235, 255, 1); | |||
| font-size: 10px; | |||
| font-weight: normal; | |||
| } | |||
| .similarityText { | |||
| font-size: 12px; | |||
| font-weight: 500; | |||
| } | |||
| } | |||
| @@ -1,12 +1,55 @@ | |||
| import { ReactComponent as SelectedFilesCollapseIcon } from '@/assets/svg/selected-files-collapse.svg'; | |||
| import { Card, Collapse, Flex, Space } from 'antd'; | |||
| import { ITestingChunk } from '@/interfaces/database/knowledge'; | |||
| import { Card, Collapse, Flex, Pagination, PaginationProps, Space } from 'antd'; | |||
| import { useDispatch, useSelector } from 'umi'; | |||
| import { TestingModelState } from '../model'; | |||
| import styles from './index.less'; | |||
| import SelectFiles from './select-files'; | |||
| import styles from './index.less'; | |||
| const similarityList: Array<{ field: keyof ITestingChunk; label: string }> = [ | |||
| { field: 'similarity', label: 'Hybrid Similarity' }, | |||
| { field: 'term_similarity', label: 'Term Similarity' }, | |||
| { field: 'vector_similarity', label: 'Vector Similarity' }, | |||
| ]; | |||
| const ChunkTitle = ({ item }: { item: ITestingChunk }) => { | |||
| return ( | |||
| <Flex gap={10}> | |||
| {similarityList.map((x) => ( | |||
| <Space key={x.field}> | |||
| <span className={styles.similarityCircle}> | |||
| {((item[x.field] as number) * 100).toFixed(2)}% | |||
| </span> | |||
| <span className={styles.similarityText}>Hybrid Similarity</span> | |||
| </Space> | |||
| ))} | |||
| </Flex> | |||
| ); | |||
| }; | |||
| interface IProps { | |||
| handleTesting: () => Promise<any>; | |||
| } | |||
| const TestingResult = ({ handleTesting }: IProps) => { | |||
| const { | |||
| documents, | |||
| chunks, | |||
| total, | |||
| pagination, | |||
| selectedDocumentIds, | |||
| }: TestingModelState = useSelector((state: any) => state.testingModel); | |||
| const dispatch = useDispatch(); | |||
| const list = [1, 2, 3, 4]; | |||
| const onChange: PaginationProps['onChange'] = (pageNumber, pageSize) => { | |||
| console.log('Page: ', pageNumber, pageSize); | |||
| dispatch({ | |||
| type: 'testingModel/setPagination', | |||
| payload: { current: pageNumber, pageSize }, | |||
| }); | |||
| handleTesting(); | |||
| }; | |||
| const TestingResult = () => { | |||
| return ( | |||
| <section className={styles.testingResultWrapper}> | |||
| <Collapse | |||
| @@ -23,7 +66,10 @@ const TestingResult = () => { | |||
| align="center" | |||
| className={styles.selectFilesTitle} | |||
| > | |||
| <span>4/25 Files Selected</span> | |||
| <span> | |||
| {selectedDocumentIds?.length ?? 0}/{documents.length} Files | |||
| Selected | |||
| </span> | |||
| <Space size={52}> | |||
| <b>Hits</b> | |||
| <b>View</b> | |||
| @@ -32,21 +78,33 @@ const TestingResult = () => { | |||
| ), | |||
| children: ( | |||
| <div> | |||
| <SelectFiles></SelectFiles> | |||
| <SelectFiles handleTesting={handleTesting}></SelectFiles> | |||
| </div> | |||
| ), | |||
| }, | |||
| ]} | |||
| /> | |||
| <Flex gap={'large'} vertical> | |||
| {list.map((x) => ( | |||
| <Card key={x} title="Default size card" extra={<a href="#">More</a>}> | |||
| <p>Card content</p> | |||
| <p>Card content</p> | |||
| <p>Card content</p> | |||
| <Flex | |||
| gap={'large'} | |||
| vertical | |||
| flex={1} | |||
| className={styles.selectFilesCollapse} | |||
| > | |||
| {chunks.map((x) => ( | |||
| <Card key={x.chunk_id} title={<ChunkTitle item={x}></ChunkTitle>}> | |||
| <div>{x.content_with_weight}</div> | |||
| </Card> | |||
| ))} | |||
| </Flex> | |||
| <Pagination | |||
| size={'small'} | |||
| showQuickJumper | |||
| current={pagination.current} | |||
| pageSize={pagination.pageSize} | |||
| total={total} | |||
| showSizeChanger | |||
| onChange={onChange} | |||
| /> | |||
| </section> | |||
| ); | |||
| }; | |||
| @@ -1,80 +1,71 @@ | |||
| import { ReactComponent as NavigationPointerIcon } from '@/assets/svg/navigation-pointer.svg'; | |||
| import { ITestingDocument } from '@/interfaces/database/knowledge'; | |||
| import { api_host } from '@/utils/api'; | |||
| import { Table, TableProps } from 'antd'; | |||
| import { useDispatch, useSelector } from 'umi'; | |||
| interface DataType { | |||
| key: string; | |||
| name: string; | |||
| hits: number; | |||
| address: string; | |||
| tags: string[]; | |||
| interface IProps { | |||
| handleTesting: () => Promise<any>; | |||
| } | |||
| const SelectFiles = () => { | |||
| const columns: TableProps<DataType>['columns'] = [ | |||
| const SelectFiles = ({ handleTesting }: IProps) => { | |||
| const documents: ITestingDocument[] = useSelector( | |||
| (state: any) => state.testingModel.documents, | |||
| ); | |||
| const dispatch = useDispatch(); | |||
| const columns: TableProps<ITestingDocument>['columns'] = [ | |||
| { | |||
| title: 'Name', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| dataIndex: 'doc_name', | |||
| key: 'doc_name', | |||
| render: (text) => <p>{text}</p>, | |||
| }, | |||
| { | |||
| title: 'Hits', | |||
| dataIndex: 'hits', | |||
| key: 'hits', | |||
| dataIndex: 'count', | |||
| key: 'count', | |||
| width: 80, | |||
| }, | |||
| { | |||
| title: 'View', | |||
| key: 'view', | |||
| width: 50, | |||
| render: () => <NavigationPointerIcon />, | |||
| render: (_, { doc_id }) => ( | |||
| <a | |||
| href={`${api_host}/document/get/${doc_id}`} | |||
| target="_blank" | |||
| rel="noreferrer" | |||
| > | |||
| <NavigationPointerIcon /> | |||
| </a> | |||
| ), | |||
| }, | |||
| ]; | |||
| const rowSelection = { | |||
| onChange: (selectedRowKeys: React.Key[], selectedRows: DataType[]) => { | |||
| console.log( | |||
| `selectedRowKeys: ${selectedRowKeys}`, | |||
| 'selectedRows: ', | |||
| selectedRows, | |||
| ); | |||
| onChange: (selectedRowKeys: React.Key[]) => { | |||
| dispatch({ | |||
| type: 'testingModel/setSelectedDocumentIds', | |||
| payload: selectedRowKeys, | |||
| }); | |||
| handleTesting(); | |||
| }, | |||
| getCheckboxProps: (record: DataType) => ({ | |||
| disabled: record.name === 'Disabled User', // Column configuration not to be checked | |||
| name: record.name, | |||
| getCheckboxProps: (record: ITestingDocument) => ({ | |||
| disabled: record.doc_name === 'Disabled User', // Column configuration not to be checked | |||
| name: record.doc_name, | |||
| }), | |||
| }; | |||
| const data: DataType[] = [ | |||
| { | |||
| key: '1', | |||
| name: 'John Brown', | |||
| hits: 32, | |||
| address: 'New York No. 1 Lake Park', | |||
| tags: ['nice', 'developer'], | |||
| }, | |||
| { | |||
| key: '2', | |||
| name: 'Jim Green', | |||
| hits: 42, | |||
| address: 'London No. 1 Lake Park', | |||
| tags: ['loser'], | |||
| }, | |||
| { | |||
| key: '3', | |||
| name: 'Joe Black', | |||
| hits: 32, | |||
| address: 'Sydney No. 1 Lake Park', | |||
| tags: ['cool', 'teacher'], | |||
| }, | |||
| ]; | |||
| return ( | |||
| <Table | |||
| columns={columns} | |||
| dataSource={data} | |||
| dataSource={documents} | |||
| showHeader={false} | |||
| rowSelection={rowSelection} | |||
| rowKey={'doc_id'} | |||
| /> | |||
| ); | |||
| }; | |||
| @@ -1,4 +1,4 @@ | |||
| let api_host = `http://223.111.148.200:9380/v1`; | |||
| let api_host = `http://123.60.95.134:9380/v1`; | |||
| export { api_host }; | |||