diff --git a/web/package-lock.json b/web/package-lock.json index 194342df9a88b95a4040aea3285cbbefc789e262..3286ade267285ac5e8879a25f274e145211411ad 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,6 +17,7 @@ "jsencrypt": "^3.3.2", "lodash": "^4.17.21", "moment": "^2.30.1", + "rc-tween-one": "^3.0.6", "react-i18next": "^14.0.0", "react-infinite-scroll-component": "^6.1.0", "umi": "^4.0.90", @@ -6508,6 +6509,16 @@ "type": "^1.0.1" } }, + "node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "node_modules/d3-polygon": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-1.0.6.tgz", + "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -6994,6 +7005,11 @@ "dva-core": "^1.1.0 || ^1.5.0-0 || ^1.6.0-0" } }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + }, "node_modules/easy-icons": { "version": "1.1.5", "resolved": "https://registry.npmmirror.com/easy-icons/-/easy-icons-1.1.5.tgz", @@ -8324,6 +8340,24 @@ "deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.", "dev": true }, + "node_modules/flubber": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/flubber/-/flubber-0.4.2.tgz", + "integrity": "sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw==", + "dependencies": { + "d3-array": "^1.2.0", + "d3-polygon": "^1.0.3", + "earcut": "^2.1.1", + "svg-path-properties": "^0.2.1", + "svgpath": "^2.2.1", + "topojson-client": "^3.0.0" + } + }, + "node_modules/flubber/node_modules/svg-path-properties": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/svg-path-properties/-/svg-path-properties-0.2.2.tgz", + "integrity": "sha512-GmrB+b6woz6CCdQe6w1GHs/1lt25l7SR5hmhF8jRdarpv/OgjLyuQygLu1makJapixeb1aQhP/Oa1iKi93o/aQ==" + }, "node_modules/follow-redirects": { "version": "1.15.4", "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.4.tgz", @@ -11847,6 +11881,11 @@ "node": ">=0.12" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz", @@ -12777,6 +12816,14 @@ "node": ">=8" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/ramda": { "version": "0.27.2", "resolved": "https://registry.npmmirror.com/ramda/-/ramda-0.27.2.tgz", @@ -13325,6 +13372,23 @@ "react-dom": "*" } }, + "node_modules/rc-tween-one": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/rc-tween-one/-/rc-tween-one-3.0.6.tgz", + "integrity": "sha512-5zTSXyyv7bahDBQ/kJw/kNxxoBqTouttoelw8FOVOyWqmTMndizJEpvaj1N+yES5Xjss6Y2iVw+9vSJQZE8Z6g==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "style-utils": "^0.3.4", + "tween-one": "^1.0.50" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/rc-upload": { "version": "4.5.2", "resolved": "https://registry.npmmirror.com/rc-upload/-/rc-upload-4.5.2.tgz", @@ -15285,6 +15349,11 @@ "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", "peer": true }, + "node_modules/style-utils": { + "version": "0.3.8", + "resolved": "https://registry.npmmirror.com/style-utils/-/style-utils-0.3.8.tgz", + "integrity": "sha512-RmGftIhY4tqtD1ERwKsVEDlt/M6UyxN/rcr95UmlooWmhtL0RwVUYJkpo1kSx3ppd9/JZzbknhy742zbMAawjQ==" + }, "node_modules/stylelint": { "version": "14.16.1", "resolved": "https://registry.npmmirror.com/stylelint/-/stylelint-14.16.1.tgz", @@ -15467,6 +15536,11 @@ "resolved": "https://registry.npmmirror.com/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" }, + "node_modules/svg-path-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/svg-path-properties/-/svg-path-properties-1.3.0.tgz", + "integrity": "sha512-R1+z37FrqyS3UXDhajNfvMxKI0smuVdedqOo4YbAQUfGqA86B9mGvr2IEXrwjjvGzCtdIKy/ad9N8m6YclaKAw==" + }, "node_modules/svg-tags": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz", @@ -15501,6 +15575,11 @@ "node": ">= 10" } }, + "node_modules/svgpath": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/svgpath/-/svgpath-2.6.0.tgz", + "integrity": "sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==" + }, "node_modules/swr": { "version": "2.2.4", "resolved": "https://registry.npmmirror.com/swr/-/swr-2.2.4.tgz", @@ -15815,6 +15894,24 @@ "resolved": "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/trim-newlines/-/trim-newlines-3.0.1.tgz", @@ -16235,6 +16332,24 @@ "resolved": "https://registry.npmmirror.com/tty-browserify/-/tty-browserify-0.0.0.tgz", "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==" }, + "node_modules/tween-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/tween-functions/-/tween-functions-1.2.0.tgz", + "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==" + }, + "node_modules/tween-one": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/tween-one/-/tween-one-1.2.7.tgz", + "integrity": "sha512-F+Z9LO9GsYqf0j5bgNhAF98RDrAZ7QjQrujJ2lVYSHl4+dBPW/atHluL2bwclZf8Vo0Yo96f6pw2uq1OGzpC/Q==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "flubber": "^0.4.2", + "raf": "^3.4.1", + "style-utils": "^0.3.6", + "svg-path-properties": "^1.0.4", + "tween-functions": "^1.2.0" + } + }, "node_modules/type": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/type/-/type-1.2.0.tgz", diff --git a/web/package.json b/web/package.json index 9f3f056bd37cbd48132382a6d02777355cf17784..8301f3fb74af2681e6725989ee02a262447b50b2 100644 --- a/web/package.json +++ b/web/package.json @@ -21,6 +21,7 @@ "jsencrypt": "^3.3.2", "lodash": "^4.17.21", "moment": "^2.30.1", + "rc-tween-one": "^3.0.6", "react-i18next": "^14.0.0", "react-infinite-scroll-component": "^6.1.0", "umi": "^4.0.90", diff --git a/web/src/hooks/knowledgeHook.ts b/web/src/hooks/knowledgeHook.ts index 3c4df8cc70fdb87ea0281e1dd788966210b092b3..9e023a9cf024d4edc44c33eabc36a85016c6ca94 100644 --- a/web/src/hooks/knowledgeHook.ts +++ b/web/src/hooks/knowledgeHook.ts @@ -1,5 +1,6 @@ import showDeleteConfirm from '@/components/deleting-confirm'; import { IKnowledge } from '@/interfaces/database/knowledge'; +import { useCallback } from 'react'; import { useDispatch, useSearchParams, useSelector } from 'umi'; export const useKnowledgeBaseId = (): string => { @@ -46,3 +47,33 @@ export const useGetDocumentDefaultParser = (knowledgeBaseId: string) => { parserConfig: item?.parser_config ?? '', }; }; + +export const useDeleteChunkByIds = (): { + removeChunk: (chunkIds: string[], documentId: string) => Promise<number>; +} => { + const dispatch = useDispatch(); + + const removeChunk = useCallback( + (chunkIds: string[], documentId: string) => () => { + return dispatch({ + type: 'chunkModel/rm_chunk', + payload: { + chunk_ids: chunkIds, + doc_id: documentId, + }, + }); + }, + [dispatch], + ); + + const onRemoveChunk = useCallback( + (chunkIds: string[], documentId: string): Promise<number> => { + return showDeleteConfirm({ onOk: removeChunk(chunkIds, documentId) }); + }, + [removeChunk], + ); + + return { + removeChunk: onRemoveChunk, + }; +}; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less index 876e426394c6687a9b44d31b9643aac560424261..3196dfac316407fd40062f33856b05c4bd7e87fc 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.less @@ -6,3 +6,11 @@ .imagePreview { width: 600px; } + +.content { + flex: 1; + em { + color: red; + font-style: normal; + } +} diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx index d4d169a5872fb2d1445f431847bb42e9408cb6cd..8936c1d9df201079fa3469161b4002722fdfa79b 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx @@ -1,7 +1,6 @@ import { IChunk } from '@/interfaces/database/knowledge'; import { api_host } from '@/utils/api'; import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd'; -import { useDispatch } from 'umi'; import { useState } from 'react'; import styles from './index.less'; @@ -9,6 +8,8 @@ import styles from './index.less'; interface IProps { item: IChunk; checked: boolean; + switchChunk: (available?: number, chunkIds?: string[]) => void; + editChunk: (chunkId: string) => void; handleCheckboxClick: (chunkId: string, checked: boolean) => void; } @@ -28,32 +29,29 @@ const Image = ({ id, className, ...props }: IImage) => { ); }; -const ChunkCard = ({ item, checked, handleCheckboxClick }: IProps) => { - const dispatch = useDispatch(); - +const ChunkCard = ({ + item, + checked, + handleCheckboxClick, + editChunk, + switchChunk, +}: IProps) => { const available = Number(item.available_int); const [enabled, setEnabled] = useState(available === 1); - const switchChunk = () => { - dispatch({ - type: 'chunkModel/switch_chunk', - payload: { - chunk_ids: [item.chunk_id], - available_int: available === 0 ? 1 : 0, - doc_id: item.doc_id, - }, - }); - }; - const onChange = (checked: boolean) => { setEnabled(checked); - switchChunk(); + switchChunk(available === 0 ? 1 : 0, [item.chunk_id]); }; const handleCheck: CheckboxProps['onChange'] = (e) => { handleCheckboxClick(item.chunk_id, e.target.checked); }; + const handleContentClick = () => { + editChunk(item.chunk_id); + }; + return ( <div> <Card> @@ -75,7 +73,13 @@ const ChunkCard = ({ item, checked, handleCheckboxClick }: IProps) => { </Popover> )} - <section>{item.content_with_weight}</section> + <section + onDoubleClick={handleContentClick} + className={styles.content} + dangerouslySetInnerHTML={{ __html: item.content_with_weight }} + > + {/* {item.content_with_weight} */} + </section> <div> <Switch checked={enabled} onChange={onChange} /> </div> diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.less b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.less new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..15f84ac2dd4a0a58a265881150b32ba890df775b --- /dev/null +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-creating-modal/index.tsx @@ -0,0 +1,131 @@ +import { useDeleteChunkByIds } from '@/hooks/knowledgeHook'; +import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; +import { DeleteOutlined } from '@ant-design/icons'; +import { Checkbox, Form, Input, Modal, Space } from 'antd'; +import React, { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'umi'; +import EditTag from '../edit-tag'; + +type FieldType = { + content?: string; +}; +interface kFProps { + doc_id: string; + chunkId: string | undefined; +} + +const ChunkCreatingModal: React.FC<kFProps> = ({ doc_id, chunkId }) => { + const dispatch = useDispatch(); + const [form] = Form.useForm(); + const isShowCreateModal: boolean = useSelector( + (state: any) => state.chunkModel.isShowCreateModal, + ); + const [checked, setChecked] = useState(false); + const [keywords, setKeywords] = useState<string[]>([]); + const loading = useOneNamespaceEffectsLoading('chunkModel', ['create_chunk']); + const { removeChunk } = useDeleteChunkByIds(); + + const handleCancel = () => { + dispatch({ + type: 'chunkModel/setIsShowCreateModal', + payload: false, + }); + }; + + const getChunk = useCallback(async () => { + console.info(chunkId); + if (chunkId && isShowCreateModal) { + const data = await dispatch<any>({ + type: 'chunkModel/get_chunk', + payload: { + chunk_id: chunkId, + }, + }); + + if (data?.retcode === 0) { + const { content_with_weight, important_kwd = [] } = data.data; + form.setFieldsValue({ content: content_with_weight }); + setKeywords(important_kwd); + } + } + + if (!chunkId) { + setKeywords([]); + form.setFieldsValue({ content: undefined }); + } + }, [chunkId, isShowCreateModal, dispatch, form]); + + useEffect(() => { + getChunk(); + }, [getChunk]); + + const handleOk = async () => { + try { + const values = await form.validateFields(); + dispatch({ + type: 'chunkModel/create_chunk', + payload: { + content_with_weight: values.content, + doc_id, + chunk_id: chunkId, + important_kwd: keywords, // keywords + }, + }); + } catch (errorInfo) { + console.log('Failed:', errorInfo); + } + }; + + const handleRemove = () => { + if (chunkId) { + return removeChunk([chunkId], doc_id); + } + }; + const handleCheck = () => { + setChecked(!checked); + }; + + return ( + <Modal + title={`${chunkId ? 'Edit' : 'Create'} Chunk`} + open={isShowCreateModal} + onOk={handleOk} + onCancel={handleCancel} + okButtonProps={{ loading }} + > + <Form + form={form} + // name="validateOnly" + autoComplete="off" + layout={'vertical'} + > + <Form.Item<FieldType> + label="Chunk" + name="content" + rules={[{ required: true, message: 'Please input value!' }]} + > + <Input.TextArea autoSize={{ minRows: 4, maxRows: 10 }} /> + </Form.Item> + </Form> + <section> + <p>Keyword*</p> + <EditTag tags={keywords} setTags={setKeywords} /> + </section> + {chunkId && ( + <section> + <p>Function*</p> + <Space size={'large'}> + <Checkbox onChange={handleCheck} checked={checked}> + Enabled + </Checkbox> + + <span onClick={handleRemove}> + <DeleteOutlined /> Delete + </span> + </Space> + </section> + )} + </Modal> + ); +}; +export default ChunkCreatingModal; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx index 8200f71d6ab2198287e6389136e9d312b604f32f..36c07e86a4eb1f0b83a40e08df6bf2c09fa9cd09 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-toolbar/index.tsx @@ -15,6 +15,7 @@ import { Button, Checkbox, Flex, + Input, Menu, MenuProps, Popover, @@ -22,7 +23,7 @@ import { RadioChangeEvent, Space, } from 'antd'; -import { useCallback, useMemo } from 'react'; +import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; import { Link, useDispatch, useSelector } from 'umi'; import { ChunkModelState } from '../../model'; @@ -30,24 +31,62 @@ interface IProps { checked: boolean; getChunkList: () => void; selectAllChunk: (checked: boolean) => void; + createChunk: () => void; + removeChunk: () => void; + switchChunk: (available: number) => void; } -const ChunkToolBar = ({ getChunkList, selectAllChunk, checked }: IProps) => { - const { documentInfo, available }: ChunkModelState = useSelector( - (state: any) => state.chunkModel, - ); +const ChunkToolBar = ({ + getChunkList, + selectAllChunk, + checked, + createChunk, + removeChunk, + switchChunk, +}: IProps) => { + const { documentInfo, available, searchString }: ChunkModelState = + useSelector((state: any) => state.chunkModel); const dispatch = useDispatch(); - const knowledgeBaseId = useKnowledgeBaseId(); + const [isShowSearchBox, setIsShowSearchBox] = useState(false); const handleSelectAllCheck = useCallback( (e: any) => { - // console.info(e.target.checked); selectAllChunk(e.target.checked); }, [selectAllChunk], ); + const handleSearchIconClick = () => { + setIsShowSearchBox(true); + }; + + const handleSearchChange: ChangeEventHandler<HTMLInputElement> = (e) => { + const val = e.target.value; + dispatch({ type: 'chunkModel/setSearchString', payload: val }); + dispatch({ + type: 'chunkModel/throttledGetChunkList', + payload: documentInfo.id, + }); + }; + + const handleSearchBlur = () => { + if (!searchString.trim()) { + setIsShowSearchBox(false); + } + }; + + const handleDelete = useCallback(() => { + removeChunk(); + }, [removeChunk]); + + const handleEnabledClick = () => { + switchChunk(1); + }; + const handleDisabledClick = () => { + switchChunk(0); + }; + const items: MenuProps['items'] = useMemo(() => { return [ { @@ -64,7 +103,7 @@ const ChunkToolBar = ({ getChunkList, selectAllChunk, checked }: IProps) => { { key: '2', label: ( - <Space> + <Space onClick={handleEnabledClick}> <CheckCircleOutlined /> <b>Enabled Selected</b> </Space> @@ -73,7 +112,7 @@ const ChunkToolBar = ({ getChunkList, selectAllChunk, checked }: IProps) => { { key: '3', label: ( - <Space> + <Space onClick={handleDisabledClick}> <CloseCircleOutlined /> <b>Disabled Selected</b> </Space> @@ -83,20 +122,21 @@ const ChunkToolBar = ({ getChunkList, selectAllChunk, checked }: IProps) => { { key: '4', label: ( - <Space> + <Space onClick={handleDelete}> <DeleteOutlined /> <b>Delete Selected</b> </Space> ), }, ]; - }, [checked, handleSelectAllCheck]); + }, [checked, handleSelectAllCheck, handleDelete]); const content = ( <Menu style={{ width: 200 }} items={items} selectable={false} /> ); const handleFilterChange = (e: RadioChangeEvent) => { + selectAllChunk(false); dispatch({ type: 'chunkModel/setAvailable', payload: e.target.value }); getChunkList(); }; @@ -129,12 +169,28 @@ const ChunkToolBar = ({ getChunkList, selectAllChunk, checked }: IProps) => { <DownOutlined /> </Button> </Popover> - <Button icon={<SearchOutlined />} /> + {isShowSearchBox ? ( + <Input + size="middle" + placeholder="Search" + prefix={<SearchOutlined />} + allowClear + onChange={handleSearchChange} + onBlur={handleSearchBlur} + value={searchString} + /> + ) : ( + <Button icon={<SearchOutlined />} onClick={handleSearchIconClick} /> + )} + <Popover content={filterContent} placement="bottom" arrow={false}> <Button icon={<FilterIcon />} /> </Popover> - <Button icon={<DeleteOutlined />} /> - <Button icon={<PlusOutlined />} type="primary" /> + <Button + icon={<PlusOutlined />} + type="primary" + onClick={() => createChunk()} + /> </Space> </Flex> ); diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/createModal.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/components/createModal.tsx deleted file mode 100644 index c771ed35674ebfbe397f22defc93fbe60d7abdc6..0000000000000000000000000000000000000000 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/components/createModal.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Form, Input, Modal } from 'antd'; -import React, { useCallback, useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'umi'; -import EditTag from './editTag'; - -type FieldType = { - content_ltks?: string; -}; -interface kFProps { - getChunkList: () => void; - isShowCreateModal: boolean; - doc_id: string; - chunk_id: string; -} - -const Index: React.FC<kFProps> = ({ - getChunkList, - doc_id, - isShowCreateModal, - chunk_id, -}) => { - const dispatch = useDispatch(); - const [form] = Form.useForm(); - - // const { , chunkInfo } = chunkModel - const [important_kwd, setImportantKwd] = useState([ - 'Unremovable', - 'Tag 2', - 'Tag 3', - ]); - const { t } = useTranslation(); - const handleCancel = () => { - dispatch({ - type: 'chunkModel/updateState', - payload: { - isShowCreateModal: false, - }, - }); - }; - - const getChunk = useCallback(async () => { - if (chunk_id && isShowCreateModal) { - const data = await dispatch<any>({ - type: 'chunkModel/get_chunk', - payload: { - chunk_id, - }, - }); - - if (data?.retcode === 0) { - const { content_ltks, important_kwd = [] } = data.data; - form.setFieldsValue({ content_ltks }); - setImportantKwd(important_kwd); - } - } - }, [chunk_id, isShowCreateModal]); - - useEffect(() => { - getChunk(); - }, [getChunk]); - - const handleOk = async () => { - try { - const values = await form.validateFields(); - dispatch({ - type: 'chunkModel/create_hunk', - payload: { - content_ltks: values.content_ltks, - doc_id, - chunk_id, - important_kwd, - }, - // callback: () => { - // dispatch({ - // type: 'chunkModel/updateState', - // payload: { - // isShowCreateModal: false, - // }, - // }); - // getChunkList && getChunkList(); - // }, - }); - } catch (errorInfo) { - console.log('Failed:', errorInfo); - } - }; - - return ( - <Modal - title="Basic Modal" - open={isShowCreateModal} - onOk={handleOk} - onCancel={handleCancel} - > - <Form - form={form} - name="validateOnly" - labelCol={{ span: 5 }} - wrapperCol={{ span: 19 }} - style={{ maxWidth: 600 }} - autoComplete="off" - > - <Form.Item<FieldType> - label="chunk 内容" - name="content_ltks" - rules={[{ required: true, message: 'Please input value!' }]} - > - <Input.TextArea /> - </Form.Item> - <EditTag tags={important_kwd} setTags={setImportantKwd} /> - </Form> - </Modal> - ); -}; -export default Index; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/edit-tag/index.less b/web/src/pages/add-knowledge/components/knowledge-chunk/components/edit-tag/index.less new file mode 100644 index 0000000000000000000000000000000000000000..6d2c327c50a0a4bf60fd6fd9ed4b5375b64df2b7 --- /dev/null +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/edit-tag/index.less @@ -0,0 +1,3 @@ +.tweenGroup { + display: inline-block; +} diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/components/edit-tag/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/components/edit-tag/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7bd3a56d8e62be26d790d8a28ebbe2ccb906d6a8 --- /dev/null +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/components/edit-tag/index.tsx @@ -0,0 +1,116 @@ +import { PlusOutlined } from '@ant-design/icons'; +import type { InputRef } from 'antd'; +import { Input, Tag, theme } from 'antd'; +import { TweenOneGroup } from 'rc-tween-one'; +import React, { useEffect, useRef, useState } from 'react'; + +import styles from './index.less'; + +interface EditTagsProps { + tags: string[]; + setTags: (tags: string[]) => void; +} + +const EditTag = ({ tags, setTags }: EditTagsProps) => { + const { token } = theme.useToken(); + const [inputVisible, setInputVisible] = useState(false); + const [inputValue, setInputValue] = useState(''); + const inputRef = useRef<InputRef>(null); + + useEffect(() => { + if (inputVisible) { + inputRef.current?.focus(); + } + }, [inputVisible]); + + const handleClose = (removedTag: string) => { + const newTags = tags.filter((tag) => tag !== removedTag); + console.log(newTags); + setTags(newTags); + }; + + const showInput = () => { + setInputVisible(true); + }; + + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setInputValue(e.target.value); + }; + + const handleInputConfirm = () => { + if (inputValue && tags.indexOf(inputValue) === -1) { + setTags([...tags, inputValue]); + } + setInputVisible(false); + setInputValue(''); + }; + + const forMap = (tag: string) => { + const tagElem = ( + <Tag + closable + onClose={(e) => { + e.preventDefault(); + handleClose(tag); + }} + > + {tag} + </Tag> + ); + return ( + <span key={tag} style={{ display: 'inline-block' }}> + {tagElem} + </span> + ); + }; + + const tagChild = tags.map(forMap); + + const tagPlusStyle: React.CSSProperties = { + background: token.colorBgContainer, + borderStyle: 'dashed', + }; + + return ( + <> + <span> + <TweenOneGroup + className={styles.tweenGroup} + enter={{ + scale: 0.8, + opacity: 0, + type: 'from', + duration: 100, + }} + onEnd={(e) => { + if (e.type === 'appear' || e.type === 'enter') { + (e.target as any).style = 'display: inline-block'; + } + }} + leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }} + appear={false} + > + {tagChild} + </TweenOneGroup> + </span> + {inputVisible ? ( + <Input + ref={inputRef} + type="text" + size="small" + style={{ width: 78 }} + value={inputValue} + onChange={handleInputChange} + onBlur={handleInputConfirm} + onPressEnter={handleInputConfirm} + /> + ) : ( + <Tag onClick={showInput} style={tagPlusStyle}> + <PlusOutlined /> + </Tag> + )} + </> + ); +}; + +export default EditTag; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/index.less b/web/src/pages/add-knowledge/components/knowledge-chunk/index.less index 76336464b95ffd2b6776702eb3b5fe2ecd50bbd5..a798eb9e2714dde665e0f5b854b38c6a47a60173 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/index.less +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/index.less @@ -23,6 +23,11 @@ } } + .chunkContainer { + height: calc(100vh - 320px); + overflow: auto; + } + .pageFooter { height: 32px; } diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx b/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx index 236ba184979f90c363d3cd9262c0706d8001a85c..e6b597459d852c90d8763295f1df95cbb3c09f38 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/index.tsx @@ -1,11 +1,11 @@ import { getOneNamespaceEffectsLoading } from '@/utils/storeUtil'; import type { PaginationProps } from 'antd'; -import { Button, Input, Pagination, Space, Spin } from 'antd'; -import { debounce } from 'lodash'; -import React, { useCallback, useEffect, useState } from 'react'; +import { Divider, Pagination, Space, Spin, message } from 'antd'; +import { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSearchParams, useSelector } from 'umi'; -import CreateModal from './components/createModal'; +import CreatingModal from './components/chunk-creating-modal'; +import { useDeleteChunkByIds } from '@/hooks/knowledgeHook'; import ChunkCard from './components/chunk-card'; import ChunkToolBar from './components/chunk-toolbar'; import styles from './index.less'; @@ -21,16 +21,9 @@ const Chunk = () => { const chunkModel: ChunkModelState = useSelector( (state: any) => state.chunkModel, ); - const [keywords, SetKeywords] = useState(''); const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]); const [searchParams] = useSearchParams(); - const { - data = [], - total, - chunk_id, - isShowCreateModal, - pagination, - } = chunkModel; + const { data = [], total, pagination } = chunkModel; const effects = useSelector((state: any) => state.loading.effects); const loading = getOneNamespaceEffectsLoading('chunkModel', effects, [ 'create_hunk', @@ -38,8 +31,10 @@ const Chunk = () => { 'switch_chunk', ]); const documentId: string = searchParams.get('doc_id') || ''; + const [chunkId, setChunkId] = useState<string | undefined>(); + const { removeChunk } = useDeleteChunkByIds(); - const getChunkList = () => { + const getChunkList = useCallback(() => { const payload: PayloadType = { doc_id: documentId, }; @@ -50,30 +45,19 @@ const Chunk = () => { ...payload, }, }); - }; - - const confirm = async (id: string) => { - const retcode = await dispatch<any>({ - type: 'chunkModel/rm_chunk', - payload: { - chunk_ids: [id], - }, - }); + }, [dispatch, documentId]); - retcode === 0 && getChunkList(); - }; + const handleEditChunk = useCallback( + (chunk_id?: string) => { + setChunkId(chunk_id); - const handleEditchunk = (chunk_id?: string) => { - dispatch({ - type: 'chunkModel/updateState', - payload: { - isShowCreateModal: true, - chunk_id, - doc_id: documentId, - }, - }); - getChunkList(); - }; + dispatch({ + type: 'chunkModel/setIsShowCreateModal', + payload: true, + }); + }, + [dispatch], + ); const onPaginationChange: PaginationProps['onShowSizeChange'] = ( page, @@ -93,9 +77,6 @@ const Chunk = () => { const selectAllChunk = useCallback( (checked: boolean) => { setSelectedChunkIds(checked ? data.map((x) => x.chunk_id) : []); - // setSelectedChunkIds((previousIds) => { - // return checked ? [...previousIds, ...data.map((x) => x.chunk_id)] : []; - // }); }, [data], ); @@ -115,6 +96,46 @@ const Chunk = () => { }, [], ); + const showSelectedChunkWarning = () => { + message.warning('Please select chunk!'); + }; + + const handleRemoveChunk = useCallback(async () => { + if (selectedChunkIds.length > 0) { + const resCode: number = await removeChunk(selectedChunkIds, documentId); + if (resCode === 0) { + setSelectedChunkIds([]); + } + } else { + showSelectedChunkWarning(); + } + }, [selectedChunkIds, documentId, removeChunk]); + + const switchChunk = useCallback( + async (available?: number, chunkIds?: string[]) => { + let ids = chunkIds; + if (!chunkIds) { + ids = selectedChunkIds; + if (selectedChunkIds.length === 0) { + showSelectedChunkWarning(); + return; + } + } + + const resCode: number = await dispatch<any>({ + type: 'chunkModel/switch_chunk', + payload: { + chunk_ids: ids, + available_int: available, + doc_id: documentId, + }, + }); + if (!chunkIds && resCode === 0) { + getChunkList(); + } + }, + [dispatch, documentId, getChunkList, selectedChunkIds], + ); useEffect(() => { getChunkList(); @@ -123,22 +144,7 @@ const Chunk = () => { type: 'chunkModel/resetFilter', // TODO: need to reset state uniformly }); }; - }, [documentId]); - - const debounceChange = debounce(getChunkList, 300); - const debounceCallback = useCallback( - (value: string) => debounceChange(value), - [], - ); - - const handleInputChange = ( - e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, - ) => { - setSelectedChunkIds([]); - const value = e.target.value; - SetKeywords(value); - debounceCallback(value); - }; + }, [dispatch, getChunkList]); return ( <> @@ -146,36 +152,27 @@ const Chunk = () => { <ChunkToolBar getChunkList={getChunkList} selectAllChunk={selectAllChunk} + createChunk={handleEditChunk} + removeChunk={handleRemoveChunk} checked={selectedChunkIds.length === data.length} + switchChunk={switchChunk} ></ChunkToolBar> - <div className={styles.filter}> - <div> - <Input - placeholder="ćśç´˘" - style={{ width: 220 }} - value={keywords} - allowClear - onChange={handleInputChange} - /> - </div> - <Button - onClick={() => { - handleEditchunk(); - }} - type="link" - > - ć·»ĺŠ ĺ†ć®µ - </Button> - </div> + <Divider></Divider> <div className={styles.pageContent}> <Spin spinning={loading} className={styles.spin} size="large"> - <Space direction="vertical" size={'middle'}> + <Space + direction="vertical" + size={'middle'} + className={styles.chunkContainer} + > {data.map((item) => ( <ChunkCard item={item} key={item.chunk_id} + editChunk={handleEditChunk} checked={selectedChunkIds.some((x) => x === item.chunk_id)} handleCheckboxClick={handleSingleCheckboxClick} + switchChunk={switchChunk} ></ChunkCard> ))} </Space> @@ -188,19 +185,14 @@ const Chunk = () => { showQuickJumper showSizeChanger onChange={onPaginationChange} - defaultPageSize={10} + pageSize={pagination.pageSize} pageSizeOptions={[10, 30, 60, 90]} - defaultCurrent={pagination.current} + current={pagination.current} total={total} /> </div> </div> - <CreateModal - doc_id={documentId} - isShowCreateModal={isShowCreateModal} - chunk_id={chunk_id} - getChunkList={getChunkList} - /> + <CreatingModal doc_id={documentId} chunkId={chunkId} /> </> ); }; diff --git a/web/src/pages/add-knowledge/components/knowledge-chunk/model.ts b/web/src/pages/add-knowledge/components/knowledge-chunk/model.ts index 2df2af8814bba11b88ccdb9e9c1c976f947a1e63..5fa2a01c818f50f0eea25de2d21e2f6fc2c4ccd7 100644 --- a/web/src/pages/add-knowledge/components/knowledge-chunk/model.ts +++ b/web/src/pages/add-knowledge/components/knowledge-chunk/model.ts @@ -2,6 +2,7 @@ import { BaseState } from '@/interfaces/common'; import { IKnowledgeFile } from '@/interfaces/database/knowledge'; import kbService from '@/services/kbService'; import { message } from 'antd'; +import { pick } from 'lodash'; // import { delay } from '@/utils/storeUtil'; import { DvaModel } from 'umi'; @@ -40,6 +41,13 @@ const model: DvaModel<ChunkModelState> = { ...payload, }; }, + setIsShowCreateModal(state, { payload }) { + return { + ...state, + isShowCreateModal: + typeof payload === 'boolean' ? payload : !state.isShowCreateModal, + }; + }, setAvailable(state, { payload }) { return { ...state, available: payload }; }, @@ -49,7 +57,7 @@ const model: DvaModel<ChunkModelState> = { setPagination(state, { payload }) { return { ...state, pagination: { ...state.pagination, ...payload } }; }, - resetFilter(state, { payload }) { + resetFilter(state, {}) { return { ...state, pagination: { @@ -84,7 +92,13 @@ const model: DvaModel<ChunkModelState> = { }); } }, - *switch_chunk({ payload = {} }, { call, put }) { + throttledGetChunkList: [ + function* ({ payload }, { put }) { + yield put({ type: 'chunk_list', payload: { doc_id: payload } }); + }, + { type: 'throttle', ms: 1000 }, // TODO: Provide type support for this effect + ], + *switch_chunk({ payload = {} }, { call }) { const { data } = yield call(kbService.switch_chunk, payload); const { retcode } = data; if (retcode === 0) { @@ -93,15 +107,21 @@ const model: DvaModel<ChunkModelState> = { return retcode; }, *rm_chunk({ payload = {} }, { call, put }) { - console.log('shanchu'); - const { data, response } = yield call(kbService.rm_chunk, payload); - const { retcode, data: res, retmsg } = data; - + const { data } = yield call(kbService.rm_chunk, payload); + const { retcode } = data; + if (retcode === 0) { + yield put({ + type: 'setIsShowCreateModal', + payload: false, + }); + yield put({ type: 'setPagination', payload: { current: 1 } }); + yield put({ type: 'chunk_list', payload: pick(payload, ['doc_id']) }); + } return retcode; }, *get_chunk({ payload = {} }, { call, put }) { - const { data, response } = yield call(kbService.get_chunk, payload); - const { retcode, data: res, retmsg } = data; + const { data } = yield call(kbService.get_chunk, payload); + const { retcode, data: res } = data; if (retcode === 0) { yield put({ type: 'updateState', @@ -112,20 +132,20 @@ const model: DvaModel<ChunkModelState> = { } return data; }, - *create_hunk({ payload = {} }, { call, put }) { + *create_chunk({ payload = {} }, { call, put }) { let service = kbService.create_chunk; if (payload.chunk_id) { service = kbService.set_chunk; } - const { data, response } = yield call(service, payload); - const { retcode, data: res, retmsg } = data; + const { data } = yield call(service, payload); + const { retcode } = data; if (retcode === 0) { yield put({ - type: 'updateState', - payload: { - isShowCreateModal: false, - }, + type: 'setIsShowCreateModal', + payload: false, }); + + yield put({ type: 'chunk_list', payload: pick(payload, ['doc_id']) }); } }, }, diff --git a/web/src/pages/add-knowledge/components/knowledge-search/index.tsx b/web/src/pages/add-knowledge/components/knowledge-search/index.tsx index 170e9fbf146e6860da8d8b89a6d480847bc46d8f..6c17135ce60f37b0f2c9f8d8b584dea0a04b4d9c 100644 --- a/web/src/pages/add-knowledge/components/knowledge-search/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-search/index.tsx @@ -17,7 +17,7 @@ import { import { debounce } from 'lodash'; import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'umi'; -import CreateModal from '../knowledge-chunk/components/createModal'; +import CreateModal from '../knowledge-chunk/components/chunk-creating-modal'; import styles from './index.less'; @@ -265,7 +265,6 @@ const KnowledgeSearching = () => { </div> </div> <CreateModal - getChunkList={getChunkList} isShowCreateModal={isShowCreateModal} chunk_id={chunk_id} doc_id={doc_id} diff --git a/web/src/pages/add-knowledge/components/knowledge-search/model.ts b/web/src/pages/add-knowledge/components/knowledge-search/model.ts index da9a4acae82bd68467f2d1118df795a6a8a0c927..592417223c6c96c100845e97015cc2b506917892 100644 --- a/web/src/pages/add-knowledge/components/knowledge-search/model.ts +++ b/web/src/pages/add-knowledge/components/knowledge-search/model.ts @@ -138,8 +138,8 @@ const model: DvaModel<KSearchModelState> = { if (payload.chunk_id) { service = kbService.set_chunk; } - const { data, response } = yield call(service, payload); - const { retcode, data: res, retmsg } = data; + const { data } = yield call(service, payload); + const { retcode } = data; yield put({ type: 'updateState', payload: {