Explorar el Código

Feat/segment add tag (#907)

tags/0.3.15
zxhlyh hace 2 años
padre
commit
4420281d96
No account linked to committer's email address

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

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 Ver fichero

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

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

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'

+ 2
- 1
web/global.d.ts Ver fichero

declare module 'lamejs';
declare module 'lamejs';
declare module 'react-18-input-autosize';

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

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: ',

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

segment: { segment: {
paragraphs: '段落', paragraphs: '段落',
keywords: '关键词', keywords: '关键词',
addKeyWord: '添加关键词',
keywordError: '关键词最大长度为 20',
characters: '字符', characters: '字符',
hitCount: '命中次数', hitCount: '命中次数',
vectorHash: '向量哈希:', vectorHash: '向量哈希:',

+ 1
- 0
web/models/datasets.ts Ver fichero

export type SegmentUpdator = { export type SegmentUpdator = {
content: string content: string
answer?: string answer?: string
keywords?: string[]
} }

+ 2
- 1
web/package.json Ver fichero

"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",

Cargando…
Cancelar
Guardar