mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-11 03:29:02 +08:00
Feat/segment add tag (#907)
This commit is contained in:
parent
d9afebe216
commit
4420281d96
94
web/app/components/base/tag-input/index.tsx
Normal file
94
web/app/components/base/tag-input/index.tsx
Normal file
@ -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'
|
||||
|
3
web/global.d.ts
vendored
3
web/global.d.ts
vendored
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user