mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-14 03:25:57 +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 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(() => {
|
||||||
@ -45,7 +46,7 @@ export const SegmentIndexTag: FC<{ positionId: string | number; className?: stri
|
|||||||
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
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -61,14 +62,16 @@ export const SegmentDetail: FC<ISegmentDetailProps> = memo(({
|
|||||||
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 = () => {
|
||||||
@ -148,9 +151,15 @@ export const SegmentDetail: FC<ISegmentDetailProps> = memo(({
|
|||||||
<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'>
|
||||||
@ -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: '' }
|
const params: SegmentUpdator = { content: '' }
|
||||||
if (docForm === 'qa_model') {
|
if (docForm === 'qa_model') {
|
||||||
if (!question.trim())
|
if (!question.trim())
|
||||||
@ -290,6 +299,9 @@ const Completed: FC<ICompletedProps> = ({ showNewSegmentModal, onNewSegmentModal
|
|||||||
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()
|
||||||
@ -298,6 +310,7 @@ const Completed: FC<ICompletedProps> = ({ showNewSegmentModal, onNewSegmentModal
|
|||||||
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,6 +10,7 @@ import { Hash02, XClose } from '@/app/components/base/icons/src/vender/line/gene
|
|||||||
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
|
||||||
@ -29,11 +30,13 @@ const NewSegmentModal: FC<NewSegmentModalProps> = memo(({
|
|||||||
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 () => {
|
||||||
@ -54,6 +57,9 @@ const NewSegmentModal: FC<NewSegmentModalProps> = memo(({
|
|||||||
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()
|
||||||
@ -117,8 +123,10 @@ const NewSegmentModal: FC<NewSegmentModalProps> = memo(({
|
|||||||
</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='text-xs font-medium text-gray-500'>{t('datasetDocuments.segment.keywords')}</div>
|
||||||
<div className='mb-8'></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'
|
||||||
|
1
web/global.d.ts
vendored
1
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: {
|
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: ',
|
||||||
|
@ -307,6 +307,8 @@ const translation = {
|
|||||||
segment: {
|
segment: {
|
||||||
paragraphs: '段落',
|
paragraphs: '段落',
|
||||||
keywords: '关键词',
|
keywords: '关键词',
|
||||||
|
addKeyWord: '添加关键词',
|
||||||
|
keywordError: '关键词最大长度为 20',
|
||||||
characters: '字符',
|
characters: '字符',
|
||||||
hitCount: '命中次数',
|
hitCount: '命中次数',
|
||||||
vectorHash: '向量哈希:',
|
vectorHash: '向量哈希:',
|
||||||
|
@ -388,4 +388,5 @@ export type RelatedAppResponse = {
|
|||||||
export type SegmentUpdator = {
|
export type SegmentUpdator = {
|
||||||
content: string
|
content: string
|
||||||
answer?: string
|
answer?: string
|
||||||
|
keywords?: string[]
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
"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",
|
||||||
@ -80,8 +81,8 @@
|
|||||||
"@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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user