Browse Source

Feat/segment add tag (#907)

zxhlyh 1 year ago
parent
commit
4420281d96

+ 94 - 0
web/app/components/base/tag-input/index.tsx

@@ -0,0 +1,94 @@
+import { useState } from 'react'
+import type { ChangeEvent, FC, KeyboardEvent } from 'react'
+import {} from 'use-context-selector'
+import { useTranslation } from 'react-i18next'
+import AutosizeInput from 'react-18-input-autosize'
+import { X } from '@/app/components/base/icons/src/vender/line/general'
+import { useToastContext } from '@/app/components/base/toast'
+
+type TagInputProps = {
+  items: string[]
+  onChange: (items: string[]) => void
+  disableRemove?: boolean
+  disableAdd?: boolean
+}
+
+const TagInput: FC<TagInputProps> = ({
+  items,
+  onChange,
+  disableAdd,
+  disableRemove,
+}) => {
+  const { t } = useTranslation()
+  const { notify } = useToastContext()
+  const [value, setValue] = useState('')
+  const [focused, setFocused] = useState(false)
+  const handleRemove = (index: number) => {
+    const copyItems = [...items]
+    copyItems.splice(index, 1)
+
+    onChange(copyItems)
+  }
+
+  const handleKeyDown = (e: KeyboardEvent) => {
+    if (e.key === 'Enter') {
+      const valueTrimed = value.trim()
+      if (!valueTrimed || (items.find(item => item === valueTrimed)))
+        return
+
+      if (valueTrimed.length > 20) {
+        notify({ type: 'error', message: t('datasetDocuments.segment.keywordError') })
+        return
+      }
+
+      onChange([...items, valueTrimed])
+      setValue('')
+    }
+  }
+
+  const handleBlur = () => {
+    setValue('')
+    setFocused(false)
+  }
+
+  return (
+    <div className='flex flex-wrap'>
+      {
+        items.map((item, index) => (
+          <div
+            key={item}
+            className='flex items-center mr-1 mt-1 px-2 py-1 text-sm text-gray-700 rounded-lg border border-gray-200'>
+            {item}
+            {
+              !disableRemove && (
+                <X
+                  className='ml-0.5 w-3 h-3 text-gray-500 cursor-pointer'
+                  onClick={() => handleRemove(index)}
+                />
+              )
+            }
+          </div>
+        ))
+      }
+      {
+        !disableAdd && (
+          <AutosizeInput
+            inputClassName='outline-none appearance-none placeholder:text-gray-300 caret-primary-600 hover:placeholder:text-gray-400'
+            className={`
+              mt-1 py-1 rounded-lg border border-transparent text-sm max-w-[300px] overflow-hidden
+              ${focused && 'px-2 border !border-dashed !border-gray-200'}
+            `}
+            onFocus={() => setFocused(true)}
+            onBlur={handleBlur}
+            value={value}
+            onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
+            onKeyDown={handleKeyDown}
+            placeholder={t('datasetDocuments.segment.addKeyWord')}
+          />
+        )
+      }
+    </div>
+  )
+}
+
+export default TagInput

+ 19 - 6
web/app/components/datasets/documents/detail/completed/index.tsx

@@ -26,6 +26,7 @@ import { Edit03, XClose } from '@/app/components/base/icons/src/vender/line/gene
 import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common'
 import Button from '@/app/components/base/button'
 import NewSegmentModal from '@/app/components/datasets/documents/detail/new-segment-modal'
+import TagInput from '@/app/components/base/tag-input'
 
 export const SegmentIndexTag: FC<{ positionId: string | number; className?: string }> = ({ positionId, className }) => {
   const localPositionId = useMemo(() => {
@@ -45,7 +46,7 @@ export const SegmentIndexTag: FC<{ positionId: string | number; className?: stri
 type ISegmentDetailProps = {
   segInfo?: Partial<SegmentDetailModel> & { id: string }
   onChangeSwitch?: (segId: string, enabled: boolean) => Promise<void>
-  onUpdate: (segmentId: string, q: string, a: string) => void
+  onUpdate: (segmentId: string, q: string, a: string, k: string[]) => void
   onCancel: () => void
 }
 /**
@@ -61,14 +62,16 @@ export const SegmentDetail: FC<ISegmentDetailProps> = memo(({
   const [isEditing, setIsEditing] = useState(false)
   const [question, setQuestion] = useState(segInfo?.content || '')
   const [answer, setAnswer] = useState(segInfo?.answer || '')
+  const [keywords, setKeywords] = useState<string[]>(segInfo?.keywords || [])
 
   const handleCancel = () => {
     setIsEditing(false)
     setQuestion(segInfo?.content || '')
     setAnswer(segInfo?.answer || '')
+    setKeywords(segInfo?.keywords || [])
   }
   const handleSave = () => {
-    onUpdate(segInfo?.id || '', question, answer)
+    onUpdate(segInfo?.id || '', question, answer, keywords)
   }
 
   const renderContent = () => {
@@ -148,9 +151,15 @@ export const SegmentDetail: FC<ISegmentDetailProps> = memo(({
       <div className={s.keywordWrapper}>
         {!segInfo?.keywords?.length
           ? '-'
-          : segInfo?.keywords?.map((word: any) => {
-            return <div className={s.keyword}>{word}</div>
-          })}
+          : (
+            <TagInput
+              items={keywords}
+              onChange={newKeywords => setKeywords(newKeywords)}
+              disableAdd={!isEditing}
+              disableRemove={!isEditing || (keywords.length === 1)}
+            />
+          )
+        }
       </div>
       <div className={cn(s.footer, s.numberInfo)}>
         <div className='flex items-center'>
@@ -272,7 +281,7 @@ const Completed: FC<ICompletedProps> = ({ showNewSegmentModal, onNewSegmentModal
     }
   }
 
-  const handleUpdateSegment = async (segmentId: string, question: string, answer: string) => {
+  const handleUpdateSegment = async (segmentId: string, question: string, answer: string, keywords: string[]) => {
     const params: SegmentUpdator = { content: '' }
     if (docForm === 'qa_model') {
       if (!question.trim())
@@ -290,6 +299,9 @@ const Completed: FC<ICompletedProps> = ({ showNewSegmentModal, onNewSegmentModal
       params.content = question
     }
 
+    if (keywords.length)
+      params.keywords = keywords
+
     const res = await updateSegment({ datasetId, documentId, segmentId, body: params })
     notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
     onCloseModal()
@@ -298,6 +310,7 @@ const Completed: FC<ICompletedProps> = ({ showNewSegmentModal, onNewSegmentModal
         if (seg.id === segmentId) {
           seg.answer = res.data.answer
           seg.content = res.data.content
+          seg.keywords = res.data.keywords
           seg.word_count = res.data.word_count
           seg.hit_count = res.data.hit_count
           seg.index_node_hash = res.data.index_node_hash

+ 10 - 2
web/app/components/datasets/documents/detail/new-segment-modal.tsx

@@ -10,6 +10,7 @@ import { Hash02, XClose } from '@/app/components/base/icons/src/vender/line/gene
 import { ToastContext } from '@/app/components/base/toast'
 import type { SegmentUpdator } from '@/models/datasets'
 import { addSegment } from '@/service/datasets'
+import TagInput from '@/app/components/base/tag-input'
 
 type NewSegmentModalProps = {
   isShow: boolean
@@ -29,11 +30,13 @@ const NewSegmentModal: FC<NewSegmentModalProps> = memo(({
   const [question, setQuestion] = useState('')
   const [answer, setAnswer] = useState('')
   const { datasetId, documentId } = useParams()
+  const [keywords, setKeywords] = useState<string[]>([])
 
   const handleCancel = () => {
     setQuestion('')
     setAnswer('')
     onCancel()
+    setKeywords([])
   }
 
   const handleSave = async () => {
@@ -54,6 +57,9 @@ const NewSegmentModal: FC<NewSegmentModalProps> = memo(({
       params.content = question
     }
 
+    if (keywords?.length)
+      params.keywords = keywords
+
     await addSegment({ datasetId, documentId, body: params })
     notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
     handleCancel()
@@ -117,8 +123,10 @@ const NewSegmentModal: FC<NewSegmentModalProps> = memo(({
           </span>
         </div>
         <div className='mb-4 py-1.5 h-[420px] overflow-auto'>{renderContent()}</div>
-        <div className='mb-2 text-xs font-medium text-gray-500'>{t('datasetDocuments.segment.keywords')}</div>
-        <div className='mb-8'></div>
+        <div className='text-xs font-medium text-gray-500'>{t('datasetDocuments.segment.keywords')}</div>
+        <div className='mb-8'>
+          <TagInput items={keywords} onChange={newKeywords => setKeywords(newKeywords)} />
+        </div>
         <div className='flex justify-end'>
           <Button
             className='mr-2 !h-9 !px-4 !py-2 text-sm font-medium text-gray-700 !rounded-lg'

+ 2 - 1
web/global.d.ts

@@ -1 +1,2 @@
-declare module 'lamejs';
+declare module 'lamejs';
+declare module 'react-18-input-autosize';

+ 2 - 0
web/i18n/lang/dataset-documents.en.ts

@@ -308,6 +308,8 @@ const translation = {
   segment: {
     paragraphs: 'Paragraphs',
     keywords: 'Key Words',
+    addKeyWord: 'Add key word',
+    keywordError: 'The maximum length of keyword is 20',
     characters: 'characters',
     hitCount: 'hit count',
     vectorHash: 'Vector hash: ',

+ 2 - 0
web/i18n/lang/dataset-documents.zh.ts

@@ -307,6 +307,8 @@ const translation = {
   segment: {
     paragraphs: '段落',
     keywords: '关键词',
+    addKeyWord: '添加关键词',
+    keywordError: '关键词最大长度为 20',
     characters: '字符',
     hitCount: '命中次数',
     vectorHash: '向量哈希:',

+ 1 - 0
web/models/datasets.ts

@@ -388,4 +388,5 @@ export type RelatedAppResponse = {
 export type SegmentUpdator = {
   content: string
   answer?: string
+  keywords?: string[]
 }

+ 2 - 1
web/package.json

@@ -47,6 +47,7 @@
     "next": "13.3.1",
     "qs": "^6.11.1",
     "react": "^18.2.0",
+    "react-18-input-autosize": "^3.0.0",
     "react-dom": "^18.2.0",
     "react-error-boundary": "^4.0.2",
     "react-headless-pagination": "^1.1.4",
@@ -80,8 +81,8 @@
     "@types/crypto-js": "^4.1.1",
     "@types/js-cookie": "^3.0.3",
     "@types/lodash-es": "^4.17.7",
-    "@types/node": "18.15.0",
     "@types/negotiator": "^0.6.1",
+    "@types/node": "18.15.0",
     "@types/qs": "^6.9.7",
     "@types/react": "18.0.28",
     "@types/react-dom": "18.0.11",