| @@ -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 | |||
| @@ -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,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' | |||
| @@ -1 +1,2 @@ | |||
| declare module 'lamejs'; | |||
| declare module 'lamejs'; | |||
| declare module 'react-18-input-autosize'; | |||
| @@ -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: ', | |||
| @@ -307,6 +307,8 @@ const translation = { | |||
| segment: { | |||
| paragraphs: '段落', | |||
| keywords: '关键词', | |||
| addKeyWord: '添加关键词', | |||
| keywordError: '关键词最大长度为 20', | |||
| characters: '字符', | |||
| hitCount: '命中次数', | |||
| vectorHash: '向量哈希:', | |||
| @@ -388,4 +388,5 @@ export type RelatedAppResponse = { | |||
| export type SegmentUpdator = { | |||
| content: string | |||
| answer?: string | |||
| keywords?: string[] | |||
| } | |||
| @@ -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", | |||