| 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 |
| import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common' | import AutoHeightTextarea from '@/app/components/base/auto-height-textarea/common' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import NewSegmentModal from '@/app/components/datasets/documents/detail/new-segment-modal' | 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 }) => { | export const SegmentIndexTag: FC<{ positionId: string | number; className?: string }> = ({ positionId, className }) => { | ||||
| const localPositionId = useMemo(() => { | const localPositionId = useMemo(() => { | ||||
| type ISegmentDetailProps = { | type ISegmentDetailProps = { | ||||
| segInfo?: Partial<SegmentDetailModel> & { id: string } | segInfo?: Partial<SegmentDetailModel> & { id: string } | ||||
| onChangeSwitch?: (segId: string, enabled: boolean) => Promise<void> | 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 | onCancel: () => void | ||||
| } | } | ||||
| /** | /** | ||||
| const [isEditing, setIsEditing] = useState(false) | const [isEditing, setIsEditing] = useState(false) | ||||
| const [question, setQuestion] = useState(segInfo?.content || '') | const [question, setQuestion] = useState(segInfo?.content || '') | ||||
| const [answer, setAnswer] = useState(segInfo?.answer || '') | const [answer, setAnswer] = useState(segInfo?.answer || '') | ||||
| const [keywords, setKeywords] = useState<string[]>(segInfo?.keywords || []) | |||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| setIsEditing(false) | setIsEditing(false) | ||||
| setQuestion(segInfo?.content || '') | setQuestion(segInfo?.content || '') | ||||
| setAnswer(segInfo?.answer || '') | setAnswer(segInfo?.answer || '') | ||||
| setKeywords(segInfo?.keywords || []) | |||||
| } | } | ||||
| const handleSave = () => { | const handleSave = () => { | ||||
| onUpdate(segInfo?.id || '', question, answer) | |||||
| onUpdate(segInfo?.id || '', question, answer, keywords) | |||||
| } | } | ||||
| const renderContent = () => { | const renderContent = () => { | ||||
| <div className={s.keywordWrapper}> | <div className={s.keywordWrapper}> | ||||
| {!segInfo?.keywords?.length | {!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> | ||||
| <div className={cn(s.footer, s.numberInfo)}> | <div className={cn(s.footer, s.numberInfo)}> | ||||
| <div className='flex items-center'> | <div className='flex items-center'> | ||||
| } | } | ||||
| } | } | ||||
| const handleUpdateSegment = async (segmentId: string, question: string, answer: string) => { | |||||
| const handleUpdateSegment = async (segmentId: string, question: string, answer: string, keywords: string[]) => { | |||||
| const params: SegmentUpdator = { content: '' } | const params: SegmentUpdator = { content: '' } | ||||
| if (docForm === 'qa_model') { | if (docForm === 'qa_model') { | ||||
| if (!question.trim()) | if (!question.trim()) | ||||
| params.content = question | params.content = question | ||||
| } | } | ||||
| if (keywords.length) | |||||
| params.keywords = keywords | |||||
| const res = await updateSegment({ datasetId, documentId, segmentId, body: params }) | const res = await updateSegment({ datasetId, documentId, segmentId, body: params }) | ||||
| notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | ||||
| onCloseModal() | onCloseModal() | ||||
| if (seg.id === segmentId) { | if (seg.id === segmentId) { | ||||
| seg.answer = res.data.answer | seg.answer = res.data.answer | ||||
| seg.content = res.data.content | seg.content = res.data.content | ||||
| seg.keywords = res.data.keywords | |||||
| seg.word_count = res.data.word_count | seg.word_count = res.data.word_count | ||||
| seg.hit_count = res.data.hit_count | seg.hit_count = res.data.hit_count | ||||
| seg.index_node_hash = res.data.index_node_hash | seg.index_node_hash = res.data.index_node_hash |
| import { ToastContext } from '@/app/components/base/toast' | import { ToastContext } from '@/app/components/base/toast' | ||||
| import type { SegmentUpdator } from '@/models/datasets' | import type { SegmentUpdator } from '@/models/datasets' | ||||
| import { addSegment } from '@/service/datasets' | import { addSegment } from '@/service/datasets' | ||||
| import TagInput from '@/app/components/base/tag-input' | |||||
| type NewSegmentModalProps = { | type NewSegmentModalProps = { | ||||
| isShow: boolean | isShow: boolean | ||||
| const [question, setQuestion] = useState('') | const [question, setQuestion] = useState('') | ||||
| const [answer, setAnswer] = useState('') | const [answer, setAnswer] = useState('') | ||||
| const { datasetId, documentId } = useParams() | const { datasetId, documentId } = useParams() | ||||
| const [keywords, setKeywords] = useState<string[]>([]) | |||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| setQuestion('') | setQuestion('') | ||||
| setAnswer('') | setAnswer('') | ||||
| onCancel() | onCancel() | ||||
| setKeywords([]) | |||||
| } | } | ||||
| const handleSave = async () => { | const handleSave = async () => { | ||||
| params.content = question | params.content = question | ||||
| } | } | ||||
| if (keywords?.length) | |||||
| params.keywords = keywords | |||||
| await addSegment({ datasetId, documentId, body: params }) | await addSegment({ datasetId, documentId, body: params }) | ||||
| notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | ||||
| handleCancel() | handleCancel() | ||||
| </span> | </span> | ||||
| </div> | </div> | ||||
| <div className='mb-4 py-1.5 h-[420px] overflow-auto'>{renderContent()}</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'> | <div className='flex justify-end'> | ||||
| <Button | <Button | ||||
| className='mr-2 !h-9 !px-4 !py-2 text-sm font-medium text-gray-700 !rounded-lg' | className='mr-2 !h-9 !px-4 !py-2 text-sm font-medium text-gray-700 !rounded-lg' |
| declare module 'lamejs'; | |||||
| declare module 'lamejs'; | |||||
| declare module 'react-18-input-autosize'; |
| segment: { | segment: { | ||||
| paragraphs: 'Paragraphs', | paragraphs: 'Paragraphs', | ||||
| keywords: 'Key Words', | keywords: 'Key Words', | ||||
| addKeyWord: 'Add key word', | |||||
| keywordError: 'The maximum length of keyword is 20', | |||||
| characters: 'characters', | characters: 'characters', | ||||
| hitCount: 'hit count', | hitCount: 'hit count', | ||||
| vectorHash: 'Vector hash: ', | vectorHash: 'Vector hash: ', |
| segment: { | segment: { | ||||
| paragraphs: '段落', | paragraphs: '段落', | ||||
| keywords: '关键词', | keywords: '关键词', | ||||
| addKeyWord: '添加关键词', | |||||
| keywordError: '关键词最大长度为 20', | |||||
| characters: '字符', | characters: '字符', | ||||
| hitCount: '命中次数', | hitCount: '命中次数', | ||||
| vectorHash: '向量哈希:', | vectorHash: '向量哈希:', |
| export type SegmentUpdator = { | export type SegmentUpdator = { | ||||
| content: string | content: string | ||||
| answer?: string | answer?: string | ||||
| keywords?: string[] | |||||
| } | } |
| "next": "13.3.1", | "next": "13.3.1", | ||||
| "qs": "^6.11.1", | "qs": "^6.11.1", | ||||
| "react": "^18.2.0", | "react": "^18.2.0", | ||||
| "react-18-input-autosize": "^3.0.0", | |||||
| "react-dom": "^18.2.0", | "react-dom": "^18.2.0", | ||||
| "react-error-boundary": "^4.0.2", | "react-error-boundary": "^4.0.2", | ||||
| "react-headless-pagination": "^1.1.4", | "react-headless-pagination": "^1.1.4", | ||||
| "@types/crypto-js": "^4.1.1", | "@types/crypto-js": "^4.1.1", | ||||
| "@types/js-cookie": "^3.0.3", | "@types/js-cookie": "^3.0.3", | ||||
| "@types/lodash-es": "^4.17.7", | "@types/lodash-es": "^4.17.7", | ||||
| "@types/node": "18.15.0", | |||||
| "@types/negotiator": "^0.6.1", | "@types/negotiator": "^0.6.1", | ||||
| "@types/node": "18.15.0", | |||||
| "@types/qs": "^6.9.7", | "@types/qs": "^6.9.7", | ||||
| "@types/react": "18.0.28", | "@types/react": "18.0.28", | ||||
| "@types/react-dom": "18.0.11", | "@types/react-dom": "18.0.11", |