Merge branch 'feat/parent-child-retrieval' of https://github.com/langgenius/dify into feat/parent-child-retrieval

This commit is contained in:
AkaraChen
2024-12-04 14:34:38 +08:00
27 changed files with 1147 additions and 127 deletions

View File

@@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="check">
<path id="Vector 1" d="M2.5 6H9.5" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 217 B

View File

@@ -1,11 +1,13 @@
.wrapper {
border-color: #d0d5dd;
.checked {
background: var(--color-components-checkbox-bg) url(./assets/check.svg) center center no-repeat;
background-size: 12px 12px;
border: none;
}
.checked {
background: #155eef url(./assets/check.svg) center center no-repeat;
.mixed {
background: var(--color-components-checkbox-bg) url(./assets/mixed.svg) center center no-repeat;
background-size: 12px 12px;
border-color: #155eef;
border: none;
}
.checked.disabled {

View File

@@ -6,16 +6,17 @@ type CheckboxProps = {
onCheck?: () => void
className?: string
disabled?: boolean
mixed?: boolean
}
const Checkbox = ({ checked, onCheck, className, disabled }: CheckboxProps) => {
const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProps) => {
return (
<div
className={cn(
s.wrapper,
'w-4 h-4 border rounded border-components-checkbox-border bg-components-checkbox-bg-unchecked shadow-xs shadow-shadow-shadow-3',
checked && s.checked,
disabled && s.disabled,
'w-4 h-4 border rounded border-gray-300',
mixed && s.mixed,
className,
)}
onClick={() => {

View File

@@ -0,0 +1,13 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group">
<path id="Vector" d="M2.5 10H0V7.5H2.5V10Z" fill="#676F83"/>
<path id="Vector_2" d="M6.25 6.25H3.75V3.75H6.25V6.25Z" fill="#676F83"/>
<path id="Vector_3" d="M2.5 6.25H0V3.75H2.5V6.25Z" fill="#676F83"/>
<path id="Vector_4" d="M6.25 2.5H3.75V0H6.25V2.5Z" fill="#676F83"/>
<path id="Vector_5" d="M2.5 2.5H0V0H2.5V2.5Z" fill="#676F83"/>
<path id="Vector_6" d="M10 2.5H7.5V0H10V2.5Z" fill="#676F83"/>
<path id="Vector_7" d="M9.58342 7.91663H7.91675V9.58329H9.58342V7.91663Z" fill="#676F83"/>
<path id="Vector_8" d="M9.58342 4.16663H7.91675V5.83329H9.58342V4.16663Z" fill="#676F83"/>
<path id="Vector_9" d="M5.83341 7.91663H4.16675V9.58329H5.83341V7.91663Z" fill="#676F83"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 792 B

View File

@@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon L">
<g id="Vector">
<path d="M2.66602 11.3333H0.666016L3.33268 8.66667L5.99935 11.3333H3.99935L3.99935 14H2.66602L2.66602 11.3333Z" fill="#354052"/>
<path d="M2.66602 4.66667L2.66602 2L3.99935 2L3.99935 4.66667L5.99935 4.66667L3.33268 7.33333L0.666016 4.66667L2.66602 4.66667Z" fill="#354052"/>
<path d="M7.33268 2.66667H13.9993V4H7.33268V2.66667ZM7.33268 12H13.9993V13.3333H7.33268V12ZM5.99935 7.33333H13.9993V8.66667H5.99935V7.33333Z" fill="#354052"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 579 B

View File

@@ -0,0 +1,116 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "10",
"height": "10",
"viewBox": "0 0 10 10",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Group"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"d": "M2.5 10H0V7.5H2.5V10Z",
"fill": "#676F83"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_2",
"d": "M6.25 6.25H3.75V3.75H6.25V6.25Z",
"fill": "#676F83"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_3",
"d": "M2.5 6.25H0V3.75H2.5V6.25Z",
"fill": "#676F83"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_4",
"d": "M6.25 2.5H3.75V0H6.25V2.5Z",
"fill": "#676F83"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_5",
"d": "M2.5 2.5H0V0H2.5V2.5Z",
"fill": "#676F83"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_6",
"d": "M10 2.5H7.5V0H10V2.5Z",
"fill": "#676F83"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_7",
"d": "M9.58342 7.91663H7.91675V9.58329H9.58342V7.91663Z",
"fill": "#676F83"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_8",
"d": "M9.58342 4.16663H7.91675V5.83329H9.58342V4.16663Z",
"fill": "#676F83"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_9",
"d": "M5.83341 7.91663H4.16675V9.58329H5.83341V7.91663Z",
"fill": "#676F83"
},
"children": []
}
]
}
]
},
"name": "Chunk"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Chunk.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Chunk'
export default Icon

View File

@@ -0,0 +1,62 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon L"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M2.66602 11.3333H0.666016L3.33268 8.66667L5.99935 11.3333H3.99935L3.99935 14H2.66602L2.66602 11.3333Z",
"fill": "#354052"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M2.66602 4.66667L2.66602 2L3.99935 2L3.99935 4.66667L5.99935 4.66667L3.33268 7.33333L0.666016 4.66667L2.66602 4.66667Z",
"fill": "#354052"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M7.33268 2.66667H13.9993V4H7.33268V2.66667ZM7.33268 12H13.9993V13.3333H7.33268V12ZM5.99935 7.33333H13.9993V8.66667H5.99935V7.33333Z",
"fill": "#354052"
},
"children": []
}
]
}
]
}
]
},
"name": "Collapse"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Collapse.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Collapse'
export default Icon

View File

@@ -1,3 +1,5 @@
export { default as Chunk } from './Chunk'
export { default as Collapse } from './Collapse'
export { default as GeneralType } from './GeneralType'
export { default as ParentChildType } from './ParentChildType'
export { default as SelectionMod } from './SelectionMod'

View File

@@ -96,7 +96,7 @@ const Tooltip: FC<TooltipProps> = ({
>
{popupContent && (<div
className={cn(
'relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg break-words',
'relative px-3 py-2 text-xs font-normal text-text-secondary bg-components-tooltip-bg rounded-md shadow-lg break-words',
popupClassName,
)}
onMouseEnter={() => triggerMethod === 'hover' && setHoverPopup()}

View File

@@ -0,0 +1,57 @@
import React, { type FC } from 'react'
import { RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLine } from '@remixicon/react'
import Divider from '@/app/components/base/divider'
type IBatchActionProps = {
selectedSegmentIds: string[]
onBatchEnable: () => Promise<void>
onBatchDisable: () => Promise<void>
onBatchDelete: () => Promise<void>
onCancel: () => void
}
const BatchAction: FC<IBatchActionProps> = ({
selectedSegmentIds,
onBatchEnable,
onBatchDisable,
onBatchDelete,
onCancel,
}) => {
return (
<div className='w-full flex justify-center gap-x-2 absolute bottom-16 z-20'>
<div className='flex items-center gap-x-1 p-1 rounded-[10px] bg-components-actionbar-bg-accent border border-components-actionbar-border-accent shadow-xl shadow-shadow-shadow-5 backdrop-blur-[5px]'>
<div className='inline-flex items-center gap-x-2 pl-2 pr-3 py-1'>
<span className='w-5 h-5 flex items-center justify-center px-1 py-0.5 bg-text-accent rounded-md text-text-primary-on-surface text-xs font-medium'>
{selectedSegmentIds.length}
</span>
<span className='text-text-accent text-[13px] font-semibold leading-[16px]'>Selected</span>
</div>
<Divider type='vertical' className='mx-0.5 h-3.5 bg-divider-regular' />
<div className='flex items-center gap-x-0.5 px-3 py-2'>
<RiCheckboxCircleLine className='w-4 h-4 text-components-button-ghost-text' />
<button className='px-0.5 text-components-button-ghost-text text-[13px] font-medium leading-[16px]' onClick={onBatchEnable}>
Enable
</button>
</div>
<div className='flex items-center gap-x-0.5 px-3 py-2'>
<RiCloseCircleLine className='w-4 h-4 text-components-button-ghost-text' />
<button className='px-0.5 text-components-button-ghost-text text-[13px] font-medium leading-[16px]' onClick={onBatchDisable}>
Disable
</button>
</div>
<div className='flex items-center gap-x-0.5 px-3 py-2'>
<RiDeleteBinLine className='w-4 h-4 text-components-button-destructive-ghost-text' />
<button className='px-0.5 text-components-button-destructive-ghost-text text-[13px] font-medium leading-[16px]' onClick={onBatchDelete}>
Delete
</button>
</div>
<Divider type='vertical' className='mx-0.5 h-3.5 bg-divider-regular' />
<button className='px-3.5 py-2 text-components-button-ghost-text text-[13px] font-medium leading-[16px]' onClick={onCancel}>
Cancel
</button>
</div>
</div>
)
}
export default React.memo(BatchAction)

View File

@@ -0,0 +1,31 @@
import React, { type FC } from 'react'
import { RiLineHeight } from '@remixicon/react'
import { useSegmentListContext } from '.'
import Tooltip from '@/app/components/base/tooltip'
import { Collapse } from '@/app/components/base/icons/src/public/knowledge'
const DisplayToggle: FC = () => {
const [isCollapsed, toggleCollapsed] = useSegmentListContext(s => [s.isCollapsed, s.toggleCollapsed])
return (
<Tooltip
popupContent={isCollapsed ? 'Expand chunks' : 'Collapse chunks'}
popupClassName='text-text-secondary system-xs-medium border-[0.5px] border-components-panel-border'
needsDelay
>
<button
className='flex items-center justify-center p-2 rounded-lg bg-components-button-secondary-bg cursor-pointer
border-[0.5px] border-components-button-secondary-border shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]'
onClick={toggleCollapsed}
>
{
isCollapsed
? <RiLineHeight className='w-4 h-4 text-components-button-secondary-text' />
: <Collapse className='w-4 h-4 text-components-button-secondary-text' />
}
</button>
</Tooltip>
)
}
export default React.memo(DisplayToggle)

View File

@@ -1,11 +1,9 @@
'use client'
import type { FC } from 'react'
import React, { memo, useEffect, useMemo, useState } from 'react'
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useDebounceFn } from 'ahooks'
import { HashtagIcon } from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { isNil, omitBy } from 'lodash-es'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import {
RiCloseLine,
RiEditLine,
@@ -14,7 +12,9 @@ import { StatusItem } from '../../list'
import { DocumentContext } from '../index'
import { ProcessStatus } from '../segment-add'
import s from './style.module.css'
import InfiniteVirtualList from './InfiniteVirtualList'
import SegmentList from './segment-list'
import DisplayToggle from './display-toggle'
import BatchAction from './batch-action'
import cn from '@/utils/classnames'
import { formatNumber } from '@/utils/format'
import Modal from '@/app/components/base/modal'
@@ -24,27 +24,44 @@ import Input from '@/app/components/base/input'
import { ToastContext } from '@/app/components/base/toast'
import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select'
import { deleteSegment, disableSegment, enableSegment, fetchSegments, updateSegment } from '@/service/datasets'
import type { SegmentDetailModel, SegmentUpdater, SegmentsQuery, SegmentsResponse } from '@/models/datasets'
import { asyncRunSafe } from '@/utils'
import type { CommonResponse } from '@/models/common'
import { updateSegment } from '@/service/datasets'
import type { ParentMode, ProcessMode, SegmentDetailModel, SegmentUpdater } from '@/models/datasets'
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'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import Checkbox from '@/app/components/base/checkbox'
import { useDeleteSegment, useDisableSegment, useEnableSegment, useSegmentList } from '@/service/knowledge/use-segment'
import { Chunk } from '@/app/components/base/icons/src/public/knowledge'
type SegmentListContextValue = {
isCollapsed: boolean
toggleCollapsed: () => void
}
const SegmentListContext = createContext({
isCollapsed: true,
toggleCollapsed: () => {},
})
export const useSegmentListContext = (selector: (value: SegmentListContextValue) => any) => {
return useContextSelector(SegmentListContext, selector)
}
export const SegmentIndexTag: FC<{ positionId: string | number; className?: string }> = ({ positionId, className }) => {
const localPositionId = useMemo(() => {
const positionIdStr = String(positionId)
if (positionIdStr.length >= 3)
return positionId
return positionIdStr.padStart(3, '0')
return `Chunk-${positionId}`
return `Chunk-${positionIdStr.padStart(2, '0')}`
}, [positionId])
return (
<div className={`text-gray-500 border border-gray-200 box-border flex items-center rounded-md italic text-[11px] pl-1 pr-1.5 font-medium ${className ?? ''}`}>
<HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
{localPositionId}
<div className={cn('flex items-center', className)}>
<Chunk className='w-3 h-3 p-[1px] text-text-tertiary mr-0.5' />
<div className='text-text-tertiary system-xs-medium'>
{localPositionId}
</div>
</div>
)
}
@@ -52,10 +69,11 @@ export const SegmentIndexTag: FC<{ positionId: string | number; className?: stri
type ISegmentDetailProps = {
embeddingAvailable: boolean
segInfo?: Partial<SegmentDetailModel> & { id: string }
onChangeSwitch?: (segId: string, enabled: boolean) => Promise<void>
onChangeSwitch?: (enabled: boolean, segId?: string) => Promise<void>
onUpdate: (segmentId: string, q: string, a: string, k: string[]) => void
onCancel: () => void
archived?: boolean
isEditing?: boolean
}
/**
* Show all the contents of the segment
@@ -67,9 +85,10 @@ const SegmentDetailComponent: FC<ISegmentDetailProps> = ({
onChangeSwitch,
onUpdate,
onCancel,
isEditing: initialIsEditing,
}) => {
const { t } = useTranslation()
const [isEditing, setIsEditing] = useState(false)
const [isEditing, setIsEditing] = useState(initialIsEditing)
const [question, setQuestion] = useState(segInfo?.content || '')
const [answer, setAnswer] = useState(segInfo?.answer || '')
const [keywords, setKeywords] = useState<string[]>(segInfo?.keywords || [])
@@ -195,7 +214,7 @@ const SegmentDetailComponent: FC<ISegmentDetailProps> = ({
size='md'
defaultValue={segInfo?.enabled}
onChange={async (val) => {
await onChangeSwitch?.(segInfo?.id || '', val)
await onChangeSwitch?.(val, segInfo?.id || '')
}}
disabled={archived}
/>
@@ -223,6 +242,8 @@ type ICompletedProps = {
onNewSegmentModalChange: (state: boolean) => void
importStatus: ProcessStatus | string | undefined
archived?: boolean
mode?: ProcessMode
parentMode?: ParentMode
// data: Array<{}> // all/part segments
}
/**
@@ -235,22 +256,26 @@ const Completed: FC<ICompletedProps> = ({
onNewSegmentModalChange,
importStatus,
archived,
mode,
parentMode,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { datasetId = '', documentId = '', docForm } = useContext(DocumentContext)
// the current segment id and whether to show the modal
const [currSegment, setCurrSegment] = useState<{ segInfo?: SegmentDetailModel; showModal: boolean }>({ showModal: false })
const [currSegment, setCurrSegment] = useState<{ segInfo?: SegmentDetailModel; showModal: boolean; isEditing?: boolean }>({ showModal: false })
const [inputValue, setInputValue] = useState<string>('') // the input value
const [searchValue, setSearchValue] = useState<string>('') // the search value
const [selectedStatus, setSelectedStatus] = useState<boolean | 'all'>('all') // the selected status, enabled/disabled/undefined
const [lastSegmentsRes, setLastSegmentsRes] = useState<SegmentsResponse | undefined>(undefined)
const [allSegments, setAllSegments] = useState<Array<SegmentDetailModel[]>>([]) // all segments data
const [loading, setLoading] = useState(false)
const [total, setTotal] = useState<number | undefined>()
const [segments, setSegments] = useState<SegmentDetailModel[]>([]) // all segments data
const [selectedSegmentIds, setSelectedSegmentIds] = useState<string[]>([])
const { eventEmitter } = useEventEmitterContextContext()
const [isCollapsed, setIsCollapsed] = useState(true)
// todo: pagination
const [currentPage, setCurrentPage] = useState(1)
const [limit, setLimit] = useState(10)
const { run: handleSearch } = useDebounceFn(() => {
setSearchValue(inputValue)
@@ -265,72 +290,86 @@ const Completed: FC<ICompletedProps> = ({
setSelectedStatus(value === 'all' ? 'all' : !!value)
}
const getSegments = async (needLastId?: boolean) => {
const finalLastId = lastSegmentsRes?.data?.[lastSegmentsRes.data.length - 1]?.id || ''
setLoading(true)
const [e, res] = await asyncRunSafe<SegmentsResponse>(fetchSegments({
const { isLoading: isLoadingSegmentList, data: segmentList, refetch: refreshSegmentList } = useSegmentList(
{
datasetId,
documentId,
params: omitBy({
last_id: !needLastId ? undefined : finalLastId,
limit: 12,
params: {
page: currentPage,
limit,
keyword: searchValue,
enabled: selectedStatus === 'all' ? 'all' : !!selectedStatus,
}, isNil) as SegmentsQuery,
}) as Promise<SegmentsResponse>)
if (!e) {
setAllSegments([...(!needLastId ? [] : allSegments), ...splitArray(res.data || [])])
setLastSegmentsRes(res)
if (!lastSegmentsRes || !needLastId)
setTotal(res?.total || 0)
}
setLoading(false)
}
},
},
mode === 'hierarchical' && parentMode === 'full-doc',
)
const resetList = () => {
setLastSegmentsRes(undefined)
setAllSegments([])
setLoading(false)
setTotal(undefined)
getSegments(false)
}
useEffect(() => {
if (segmentList)
setSegments(segmentList.data || [])
}, [segmentList])
const onClickCard = (detail: SegmentDetailModel) => {
setCurrSegment({ segInfo: detail, showModal: true })
const resetList = useCallback(() => {
setSegments([])
refreshSegmentList()
}, [])
const onClickCard = (detail: SegmentDetailModel, isEditing = false) => {
setCurrSegment({ segInfo: detail, showModal: true, isEditing })
}
const onCloseModal = () => {
setCurrSegment({ ...currSegment, showModal: false })
}
const onChangeSwitch = async (segId: string, enabled: boolean) => {
const opApi = enabled ? enableSegment : disableSegment
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, segmentId: segId }) as Promise<CommonResponse>)
if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
for (const item of allSegments) {
for (const seg of item) {
if (seg.id === segId)
seg.enabled = enabled
}
}
setAllSegments([...allSegments])
}
else {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
}
const { mutateAsync: enableSegment } = useEnableSegment()
const onDelete = async (segId: string) => {
const [e] = await asyncRunSafe<CommonResponse>(deleteSegment({ datasetId, documentId, segmentId: segId }) as Promise<CommonResponse>)
if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
resetList()
}
else {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
}
const { mutateAsync: disableSegment } = useDisableSegment()
const onChangeSwitch = useCallback(async (enable: boolean, segId?: string) => {
const operationApi = enable ? enableSegment : disableSegment
await operationApi({ datasetId, documentId, segmentIds: segId ? [segId] : selectedSegmentIds }, {
onSuccess: () => {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
for (const seg of segments) {
if (segId ? seg.id === segId : selectedSegmentIds.includes(seg.id))
seg.enabled = enable
}
setSegments([...segments])
},
onError: () => {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
},
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [datasetId, documentId, selectedSegmentIds, segments])
const { mutateAsync: deleteSegment } = useDeleteSegment()
const onDelete = useCallback(async (segId?: string) => {
await deleteSegment({ datasetId, documentId, segmentIds: segId ? [segId] : selectedSegmentIds }, {
onSuccess: () => {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
resetList()
},
onError: () => {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
},
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [datasetId, documentId, selectedSegmentIds])
const onCancelBatchOperation = useCallback(() => {
setSelectedSegmentIds([])
}, [])
const onSelected = useCallback((segId: string) => {
setSelectedSegmentIds(prev =>
prev.includes(segId)
? prev.filter(id => id !== segId)
: [...prev, segId],
)
}, [])
const handleUpdateSegment = async (segmentId: string, question: string, answer: string, keywords: string[]) => {
const params: SegmentUpdater = { content: '' }
@@ -358,40 +397,62 @@ const Completed: FC<ICompletedProps> = ({
const res = await updateSegment({ datasetId, documentId, segmentId, body: params })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onCloseModal()
for (const item of allSegments) {
for (const seg of item) {
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
seg.enabled = res.data.enabled
}
for (const seg of segments) {
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
seg.enabled = res.data.enabled
}
}
setAllSegments([...allSegments])
setSegments([...segments])
}
finally {
eventEmitter?.emit('')
}
}
useEffect(() => {
if (lastSegmentsRes !== undefined)
getSegments(false)
}, [selectedStatus, searchValue])
useEffect(() => {
if (importStatus === ProcessStatus.COMPLETED)
resetList()
}, [importStatus])
}, [importStatus, resetList])
const isAllSelected = useMemo(() => {
return segments.every(seg => selectedSegmentIds.includes(seg.id))
}, [segments, selectedSegmentIds])
const isSomeSelected = useMemo(() => {
return segments.some(seg => selectedSegmentIds.includes(seg.id))
}, [segments, selectedSegmentIds])
const onSelectedAll = useCallback(() => {
setSelectedSegmentIds((prev) => {
const currentAllSegIds = segments.map(seg => seg.id)
const prevSelectedIds = prev.filter(item => !currentAllSegIds.includes(item))
return [...prevSelectedIds, ...((isAllSelected || selectedSegmentIds.length > 0) ? [] : currentAllSegIds)]
})
}, [segments, isAllSelected, selectedSegmentIds])
const totalText = useMemo(() => {
return segmentList?.total ? formatNumber(segmentList.total) : '--'
}, [segmentList?.total])
return (
<>
<SegmentListContext.Provider value={{
isCollapsed,
toggleCollapsed: () => setIsCollapsed(!isCollapsed),
}}>
<div className={s.docSearchWrapper}>
<div className={s.totalText}>{total ? formatNumber(total) : '--'} {t('datasetDocuments.segment.paragraphs')}</div>
<Checkbox
className='shrink-0'
checked={isAllSelected}
mixed={!isAllSelected && isSomeSelected}
onCheck={onSelectedAll}
/>
<div className={cn('system-sm-semibold-uppercase pl-5', s.totalText)}>{totalText} {t('datasetDocuments.segment.chunks')}</div>
<SimpleSelect
onSelect={onChangeStatus}
items={[
@@ -401,7 +462,7 @@ const Completed: FC<ICompletedProps> = ({
]}
defaultValue={'all'}
className={s.select}
wrapperClassName='h-fit w-[120px] mr-2' />
wrapperClassName='h-fit w-[100px] mr-2' />
<Input
showLeftIcon
showClearIcon
@@ -410,13 +471,15 @@ const Completed: FC<ICompletedProps> = ({
onChange={e => handleInputChange(e.target.value)}
onClear={() => handleInputChange('')}
/>
<Divider type='vertical' className='h-3.5 mx-3' />
<DisplayToggle />
</div>
<InfiniteVirtualList
<SegmentList
embeddingAvailable={embeddingAvailable}
hasNextPage={lastSegmentsRes?.has_more ?? true}
isNextPageLoading={loading}
items={allSegments}
loadNextPage={getSegments}
isLoading={isLoadingSegmentList}
items={segments}
selectedSegmentIds={selectedSegmentIds}
onSelected={onSelected}
onChangeSwitch={onChangeSwitch}
onDelete={onDelete}
onClick={onClickCard}
@@ -426,6 +489,7 @@ const Completed: FC<ICompletedProps> = ({
<SegmentDetail
embeddingAvailable={embeddingAvailable}
segInfo={currSegment.segInfo ?? { id: '' }}
isEditing={currSegment.isEditing}
onChangeSwitch={onChangeSwitch}
onUpdate={handleUpdateSegment}
onCancel={onCloseModal}
@@ -438,7 +502,15 @@ const Completed: FC<ICompletedProps> = ({
onCancel={() => onNewSegmentModalChange(false)}
onSave={resetList}
/>
</>
{selectedSegmentIds.length > 0
&& <BatchAction
selectedSegmentIds={selectedSegmentIds}
onBatchEnable={onChangeSwitch.bind(null, true)}
onBatchDisable={onChangeSwitch.bind(null, false)}
onBatchDelete={onDelete}
onCancel={onCancelBatchOperation}
/>}
</SegmentListContext.Provider>
)
}

View File

@@ -0,0 +1,212 @@
export const mockSegments = {
data: [
{
id: '12aa196a-cf47-4962-a64a-7d927ed9b0ea',
position: 1,
document_id: '887985f1-ca0c-4805-8e9f-34cbc4738a3c',
content: 'Dify 云服务 · 自托管 · 文档 · (需用英文)常见问题解答 / 联系团队\n\nDify 是一个开源的 LLM 应用开发平台。其直观的界面结合了 AI 工作流、RAG 管道、Agent、模型管理、可观测性功能等让您可以快速从原型到生产。以下是其核心功能列表\n\n1. 工作流: 在画布上构建和测试功能强大的 AI 工作流程,利用以下所有功能以及更多功能。\n\nhttps://github.com/langgenius/dify/assets/13230914/356df23e-1604-483d-80a6-9517ece318aa\n\n2. 全面的模型支持: 与数百种专有/开源 LLMs 以及数十种推理提供商和自托管解决方案无缝集成,涵盖 GPT、Mistral、Llama3 以及任何与 OpenAI API 兼容的模型。完整的支持模型提供商列表可在此处找到。\n\n3. Prompt IDE: 用于制作提示、比较模型性能以及向基于聊天的应用程序添加其他功能(如文本转语音)的直观界面。\n\n4. RAG Pipeline: 广泛的 RAG 功能,涵盖从文档摄入到检索的所有内容,支持从 PDF、PPT 和其他常见文档格式中提取文本的开箱即用的支持。\n\n5. Agent 智能体: 您可以基于 LLM 函数调用或 ReAct 定义 Agent并为 Agent 添加预构建或自定义工具。Dify 为 AI Agent 提供了50多种内置工具如谷歌搜索、DALL·E、Stable Diffusion 和 WolframAlpha 等。',
answer: '',
word_count: 672,
tokens: 481,
keywords: [
'功能',
'AI',
'LLM',
'模型',
'文档',
'Agent',
'开源',
'Dify',
'支持',
'RAG',
],
index_node_id: 'b67972c2-4a95-4e46-bf8e-f32535bfc483',
index_node_hash: '40ead185f2ec6a451da09e99f4f5a7438df4542590090660b7f2f40099220cf0',
hit_count: 0,
enabled: true,
disabled_at: 1732081062,
disabled_by: '',
status: 'completed',
created_by: '573cfc4a-4ff1-43d2-b3e9-46ff1def08c5',
created_at: 1732081062,
indexing_at: 1732081061,
completed_at: 1732081064,
error: null,
stopped_at: 1732081062,
},
{
id: '4c701023-90a6-4df9-bc26-49cfb701badc',
position: 2,
document_id: '887985f1-ca0c-4805-8e9f-34cbc4738a3c',
content: '6. LLMOps: 随时间监视和分析应用程序日志和性能。您可以根据生产数据和标注持续改进提示、数据集和模型。\n\n7. 后端即服务: 所有 Dify 的功能都带有相应的 API因此您可以轻松地将 Dify 集成到自己的业务逻辑中。\n\n功能比较',
answer: '',
word_count: 122,
tokens: 104,
keywords: [
'标注',
'API',
'Dify',
'集成',
'LLMOps',
'后端',
'应用程序',
'数据',
'日志',
'功能',
],
index_node_id: 'fd5a3ea6-c726-41cb-bf0f-00da11f7cab9',
index_node_hash: '4e3f5f693e9e43734c12613bbb9971eae154be7765fd0b91fb7263b1755319b8',
hit_count: 0,
enabled: false,
disabled_at: 1732081062,
disabled_by: '',
status: 'completed',
created_by: '573cfc4a-4ff1-43d2-b3e9-46ff1def08c5',
created_at: 1732081062,
indexing_at: 1732081061,
completed_at: 1732081064,
error: null,
stopped_at: 1732081062,
},
{
id: '070f9780-1819-43fc-b976-780db8e19ed9',
position: 3,
document_id: '887985f1-ca0c-4805-8e9f-34cbc4738a3c',
content: '功能 Dify.AI LangChain Flowise OpenAI Assistant API 编程方法 API + 应用程序导向 Python 代码 应用程序导向 API 导向 支持的 LLMs 丰富多样 丰富多样 丰富多样 仅限 OpenAI RAG引擎 ✅ ✅ ✅ ✅ Agent ✅ ✅ ❌ ✅ 工作流 ✅ ❌ ✅ ❌ 可观测性 ✅ ✅ ❌ ❌ 企业功能SSO/访问控制) ✅ ❌ ❌ ❌ 本地部署 ✅ ✅ ✅ ❌',
answer: '',
word_count: 214,
tokens: 158,
keywords: [
'导向',
'API',
'Dify',
'OpenAI',
'AI',
'多样',
'LangChain',
'应用程序',
'Flowise',
'丰富',
],
index_node_id: 'a3c7a2bd-003a-4667-a4a8-2da6c27cd887',
index_node_hash: 'e824b23aa039ebc6a6b34a366251235bd81ad72535c2ea66fab949b1f78a65dc',
hit_count: 0,
enabled: true,
disabled_at: 1732081062,
disabled_by: '',
status: 'completed',
created_by: '573cfc4a-4ff1-43d2-b3e9-46ff1def08c5',
created_at: 1732081062,
indexing_at: 1732081061,
completed_at: 1732081064,
error: null,
stopped_at: 1732081062,
},
{
id: 'c817f359-d927-4987-b940-e040251b10e1',
position: 4,
document_id: '887985f1-ca0c-4805-8e9f-34cbc4738a3c',
content: '使用 Dify\n\n云 我们提供 Dify 云服务,任何人都可以零设置尝试。它提供了自部署版本的所有功能,并在沙盒计划中包含 200 次免费的 GPT-4 调用。\n\n自托管 Dify 社区版 使用这个入门指南快速在您的环境中运行 Dify。 使用我们的文档进行进一步的参考和更深入的说明。\n\n面向企业/组织的 Dify 我们提供额外的面向企业的功能。给我们发送电子邮件讨论企业需求。\n\n对于使用 AWS 的初创公司和中小型企业,请查看 AWS Marketplace 上的 Dify 高级版,并使用一键部署到您自己的 AWS VPC。它是一个价格实惠的 AMI 产品,提供了使用自定义徽标和品牌创建应用程序的选项。\n\n保持领先\n\n在 GitHub 上给 Dify Star并立即收到新版本的通知。\n\n安装社区版\n\n系统要求\n\n在安装 Dify 之前,请确保您的机器满足以下最低系统要求:\n\nCPU >= 2 Core\n\nRAM >= 4 GiB\n\n快速启动\n\n启动 Dify 服务器的最简单方法是运行我们的 docker-compose.yml 文件。在运行安装命令之前,请确保您的机器上安装了 Docker 和 Docker Compose\n\nbash cd docker cp .env.example .env docker compose up -d\n\n运行后可以在浏览器上访问 http://localhost/install 进入 Dify 控制台并开始初始化安装操作。\n\n自定义配置',
answer: '',
word_count: 650,
tokens: 427,
keywords: [
'Docker',
'Dify',
'env',
'AWS',
'docker',
'自定义',
'使用',
'确保您',
'安装',
'compose',
],
index_node_id: '2af623f5-dea6-4b6b-a147-17f9e76ac1dd',
index_node_hash: '7570a716c175c92b47658536e3c0df7dce8bac30b09cd33fb4333299874ebb0d',
hit_count: 0,
enabled: true,
disabled_at: 1732081062,
disabled_by: '',
status: 'completed',
created_by: '573cfc4a-4ff1-43d2-b3e9-46ff1def08c5',
created_at: 1732081062,
indexing_at: 1732081061,
completed_at: 1732081064,
error: null,
stopped_at: 1732081062,
},
{
id: 'c2cbfe0b-304c-40c2-9980-7d39d65e5b18',
position: 5,
document_id: '887985f1-ca0c-4805-8e9f-34cbc4738a3c',
content: '运行后,可以在浏览器上访问 http://localhost/install 进入 Dify 控制台并开始初始化安装操作。\n\n自定义配置\n\n如果您需要自定义配置请参考 .env.example 文件中的注释,并更新 .env 文件中对应的值。此外,您可能需要根据您的具体部署环境和需求对 docker-compose.yaml 文件本身进行调整,例如更改镜像版本、端口映射或卷挂载。完成任何更改后,请重新运行 docker-compose up -d。您可以在此处找到可用环境变量的完整列表。\n\n使用 Helm Chart 部署\n\n使用 Helm Chart 版本或者 YAML 文件,可以在 Kubernetes 上部署 Dify。\n\nHelm Chart by @LeoQuote\n\nHelm Chart by @BorisPolonsky\n\nYAML 文件 by @Winson-030\n\n使用 Terraform 部署\n\n使用 terraform 一键将 Dify 部署到云平台\n\nAzure Global\n\nAzure Terraform by @nikawang\n\nGoogle Cloud\n\nGoogle Cloud Terraform by @sotazum\n\nStar History\n\nContributing\n\n对于那些想要贡献代码的人请参阅我们的贡献指南。 同时,请考虑通过社交媒体、活动和会议来支持 Dify 的分享。\n\n我们正在寻找贡献者来帮助将Dify翻译成除了中文和英文之外的其他语言。如果您有兴趣帮助请参阅我们的i18n README获取更多信息并在我们的Discord社区服务器的global-users频道中留言。\n\nContributors\n\n社区与支持',
answer: '',
word_count: 751,
tokens: 424,
keywords: [
'Terraform',
'Dify',
'env',
'Chart',
'自定义',
'docker',
'Helm',
'部署',
'请参阅',
'文件',
],
index_node_id: 'e8b230c2-1ab6-4e70-b317-c50479b284d1',
index_node_hash: '1efe0128dc40d87f3cd57855e872e4b67f20cc71a6c52732bfd67cd5bdcff65e',
hit_count: 0,
enabled: true,
disabled_at: 1732081062,
disabled_by: '',
status: 'completed',
created_by: '573cfc4a-4ff1-43d2-b3e9-46ff1def08c5',
created_at: 1732081062,
indexing_at: 1732081061,
completed_at: 1732081064,
error: null,
stopped_at: 1732081062,
},
{
id: '0dcea77f-657d-4765-bc4a-a71806bede29',
position: 6,
document_id: '887985f1-ca0c-4805-8e9f-34cbc4738a3c',
content: 'Contributors\n\n社区与支持\n\n我们欢迎您为 Dify 做出贡献,以帮助改善 Dify。包括提交代码、问题、新想法或分享您基于 Dify 创建的有趣且有用的 AI 应用程序。同时,我们也欢迎您在不同的活动、会议和社交媒体上分享 Dify。\n\nGithub Discussion. 👉:分享您的应用程序并与社区交流。\n\nGitHub Issues。👉使用 Dify.AI 时遇到的错误和问题,请参阅贡献指南。\n\n电子邮件支持。👉关于使用 Dify.AI 的问题。\n\nDiscord。👉分享您的应用程序并与社区交流。\n\nX(Twitter)。👉:分享您的应用程序并与社区交流。\n\n商业许可。👉有关商业用途许可 Dify.AI 的商业咨询。\n\n微信 👉:扫描下方二维码,添加微信好友,备注 Dify我们将邀请您加入 Dify 社区。\n\n安全问题\n\n为了保护您的隐私请避免在 GitHub 上发布安全问题。发送问题至 security@dify.ai我们将为您做更细致的解答。\n\nLicense\n\n本仓库遵循 Dify Open Source License 开源协议,该许可证本质上是 Apache 2.0,但有一些额外的限制。',
answer: '',
word_count: 525,
tokens: 388,
keywords: [
'问题',
'Dify',
'分享',
'AI',
'GitHub',
'微信',
'应用程序',
'社区',
'欢迎您',
'License',
],
index_node_id: '3d17802d-9316-4e0d-9e9e-179f12e9830c',
index_node_hash: 'd7d3093eb73803bdbfabe811e33ff60c8b75c15340f9046cac53b2e02fa07203',
hit_count: 0,
enabled: true,
disabled_at: 1732081062,
disabled_by: '',
status: 'completed',
created_by: '573cfc4a-4ff1-43d2-b3e9-46ff1def08c5',
created_at: 1732081062,
indexing_at: 1732081061,
completed_at: 1732081064,
error: null,
stopped_at: 1732081062,
},
],
doc_form: 'text_model',
has_more: false,
limit: 10,
total: 6,
}

View File

@@ -0,0 +1,282 @@
import React, { type FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { StatusItem } from '../../list'
import DocumentFileIcon from '../../../common/document-file-icon'
import { SegmentIndexTag, useSegmentListContext } from '.'
import type { SegmentDetailModel } from '@/models/datasets'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider'
import { formatNumber } from '@/utils/format'
import Confirm from '@/app/components/base/confirm'
import cn from '@/utils/classnames'
import Badge from '@/app/components/base/badge'
const Dot = React.memo(() => {
return (
<div className='text-text-quaternary text-xs font-medium'>·</div>
)
})
Dot.displayName = 'Dot'
const ProgressBar: FC<{ percent: number; loading: boolean }> = React.memo(({ percent, loading }) => {
return (
<div className=''>
<div className=''>
<div
className=''
style={{ width: `${loading ? 0 : (Math.min(percent, 1) * 100).toFixed(2)}%` }}
/>
</div>
<div className=''>{loading ? null : percent.toFixed(2)}</div>
</div>
)
})
ProgressBar.displayName = 'ProgressBar'
type DocumentTitleProps = {
name: string
extension?: string
}
const DocumentTitle: FC<DocumentTitleProps> = React.memo(({ extension, name }) => {
return (
<div className=''>
<DocumentFileIcon name={name} extension={extension} size={'sm'} />
<span className=''>{name || '--'}</span>
</div>
)
})
DocumentTitle.displayName = 'DocumentTitle'
const Tag = React.memo(({ text }: { text: string }) => {
return (
<div className='inline-flex items-center gap-x-0.5'>
<span className='text-text-quaternary text-xs font-medium'>#</span>
<span className='text-text-tertiary text-xs'>{text}</span>
</div>
)
})
Tag.displayName = 'Tag'
export type UsageScene = 'doc' | 'hitTesting'
type ISegmentCardProps = {
loading: boolean
detail?: SegmentDetailModel & { document?: { name: string } }
contentExternal?: string
refSource?: {
title: string
uri: string
}
isExternal?: boolean
score?: number
onClick?: () => void
onChangeSwitch?: (enabled: boolean, segId?: string) => Promise<void>
onDelete?: (segId: string) => Promise<void>
onClickEdit?: () => void
scene?: UsageScene
className?: string
archived?: boolean
embeddingAvailable?: boolean
}
const SegmentCard: FC<ISegmentCardProps> = ({
detail = {},
contentExternal,
isExternal,
refSource,
score,
onClick,
onChangeSwitch,
onDelete,
onClickEdit,
loading = true,
scene = 'doc',
className = '',
archived,
embeddingAvailable,
}) => {
const { t } = useTranslation()
const {
id,
position,
enabled,
content,
word_count,
hit_count,
answer,
keywords,
} = detail as Required<ISegmentCardProps>['detail']
const [showModal, setShowModal] = useState(false)
const isDocScene = useMemo(() => {
return scene === 'doc'
}, [scene])
// todo: change to real logic
const chunkEdited = useMemo(() => {
return true
}, [])
const textOpacity = useMemo(() => {
return enabled ? '' : 'opacity-50'
}, [enabled])
const renderContent = () => {
if (answer) {
return (
<>
<div className='flex'>
<div className='w-4 mr-2 text-[13px] font-medium leading-[20px] text-text-tertiary'>Q</div>
<div className='text-text-secondary body-md-regular'>{content}</div>
</div>
<div className='flex'>
<div className='w-4 mr-2 text-[13px] font-medium leading-[20px] text-text-tertiary'>A</div>
<div className='text-text-secondary body-md-regular'>{answer}</div>
</div>
</>
)
}
if (contentExternal)
return contentExternal
return content
}
const isCollapsed = useSegmentListContext(s => s.isCollapsed)
return (
<div className={cn('p-3 pb-2.5 hover:bg-dataset-chunk-detail-card-hover-bg rounded-xl group/card', className)} onClick={() => onClick?.()}>
<div className='relative flex items-center justify-between'>
{isDocScene
? <>
<div className='flex items-center gap-x-2'>
<SegmentIndexTag positionId={position} className={textOpacity} />
<Dot />
<div className={cn('text-text-tertiary system-xs-medium', textOpacity)}>{`${formatNumber(word_count)} Characters`}</div>
<Dot />
<div className={cn('text-text-tertiary system-xs-medium', textOpacity)}>{`${formatNumber(hit_count)} Retrieval Count`}</div>
<Dot />
{chunkEdited && (
<Badge text='edited' uppercase className={textOpacity} />
)}
</div>
<div className=''>
{loading
? (
<Indicator color="gray" />
)
: (
<>
<StatusItem status={enabled ? 'enabled' : 'disabled'} reverse textCls="text-text-tertiary system-xs-regular" />
{embeddingAvailable && (
<div className="absolute -top-2.5 -right-2.5 z-20 hidden group-hover/card:flex items-center gap-x-0.5 p-1
rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-[5px]">
{!archived && (
<>
<div
className='shrink-0 w-6 h-6 flex items-center justify-center rounded-lg hover:bg-state-base-hover cursor-pointer'
onClick={(e) => {
e.stopPropagation()
onClickEdit?.()
}}>
<RiEditLine className='w-4 h-4 text-text-tertiary' />
</div>
<div className='shrink-0 w-6 h-6 flex items-center justify-center rounded-lg hover:bg-state-destructive-hover cursor-pointer group/delete'
onClick={(e) => {
e.stopPropagation()
setShowModal(true)
}
}>
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary group-hover/delete:text-text-destructive' />
</div>
<Divider type="vertical" className="h-3.5 bg-divider-regular" />
</>
)}
<div
onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
e.stopPropagation()
}
className="flex items-center"
>
<Switch
size='md'
disabled={archived || detail.status !== 'completed'}
defaultValue={enabled}
onChange={async (val) => {
await onChangeSwitch?.(val, id)
}}
/>
</div>
</div>
)}
</>
)}
</div>
</>
: (
score !== null
? (
<div className=''>
<div className='' />
<ProgressBar percent={score ?? 0} loading={loading} />
</div>
)
: null
)}
</div>
{loading
? (
<div className=''>
<div className='' />
</div>
)
: (
isDocScene
? <>
<div className={cn('text-text-secondary body-md-regular -tracking-[0.07px] mt-1', textOpacity, isCollapsed ? 'line-clamp-2' : 'line-clamp-20')}>
{renderContent()}
</div>
<div className={cn('flex items-center gap-x-2 pt-1.5', textOpacity)}>
{keywords?.map(keyword => <Tag key={keyword} text={keyword} />)}
</div>
</>
: <>
<div className='text-text-secondary body-md-regular -tracking-[0.07px]'>
{renderContent()}
</div>
<div className=''>
<Divider />
<div className="relative flex items-center w-full pb-1">
<DocumentTitle
name={detail?.document?.name || refSource?.title || ''}
extension={(detail?.document?.name || refSource?.title || '').split('.').pop() || 'txt'}
/>
<div className=''>
{isExternal ? t('datasetHitTesting.viewDetail') : t('datasetHitTesting.viewChart')}
<RiArrowRightUpLine className="w-3.5 h-3.5 ml-1" />
</div>
</div>
</div>
</>
)}
{showModal
&& <Confirm
isShow={showModal}
title={t('datasetDocuments.segment.delete')}
confirmText={t('common.operation.sure')}
onConfirm={async () => { await onDelete?.(id) }}
onCancel={() => setShowModal(false)}
/>
}
</div>
)
}
export default React.memo(SegmentCard)

View File

@@ -0,0 +1,71 @@
import type { FC } from 'react'
import React from 'react'
import SegmentCard from './segment-card'
import type { SegmentDetailModel } from '@/models/datasets'
import Checkbox from '@/app/components/base/checkbox'
import Loading from '@/app/components/base/loading'
import Divider from '@/app/components/base/divider'
type ISegmentListProps = {
isLoading: boolean
items: SegmentDetailModel[]
selectedSegmentIds: string[]
onSelected: (segId: string) => void
onClick: (detail: SegmentDetailModel, isEditing?: boolean) => void
onChangeSwitch: (enabled: boolean, segId?: string,) => Promise<void>
onDelete: (segId: string) => Promise<void>
archived?: boolean
embeddingAvailable: boolean
}
const SegmentList: FC<ISegmentListProps> = ({
isLoading,
items,
selectedSegmentIds,
onSelected,
onClick: onClickCard,
onChangeSwitch,
onDelete,
archived,
embeddingAvailable,
}) => {
if (isLoading)
return <Loading type='app' />
return (
<div className='flex flex-col h-full overflow-y-auto'>
{
items.map((segItem) => {
const isLast = items[items.length - 1].id === segItem.id
return (
<div key={segItem.id} className='flex items-start gap-x-2'>
<Checkbox
key={`${segItem.id}-checkbox`}
className='shrink-0 mt-3.5'
checked={selectedSegmentIds.includes(segItem.id)}
onCheck={() => onSelected(segItem.id)}
/>
<div className='flex flex-col'>
<SegmentCard
key={`${segItem.id}-card`}
detail={segItem}
onClick={() => onClickCard(segItem)}
onChangeSwitch={onChangeSwitch}
onClickEdit={() => onClickCard(segItem, true)}
onDelete={onDelete}
loading={false}
archived={archived}
embeddingAvailable={embeddingAvailable}
/>
{!isLast && <div className='w-full px-3'>
<Divider type='horizontal' className='bg-divider-subtle my-1' />
</div>}
</div>
</div>
)
})
}
</div>
)
}
export default SegmentList

View File

@@ -5,10 +5,10 @@
grid-auto-rows: 180px;
} */
.totalText {
@apply text-gray-900 font-medium text-base flex-1;
@apply text-text-secondary flex-1;
}
.docSearchWrapper {
@apply sticky w-full py-1 -top-3 bg-white flex items-center mb-3 justify-between z-10 flex-wrap gap-y-1;
@apply sticky w-full -top-3 bg-white flex items-center mb-3 justify-between z-10 flex-wrap gap-y-1;
}
.listContainer {
height: calc(100% - 3.25rem);

View File

@@ -195,6 +195,7 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
className={style.layoutRightIcon}
onClick={() => setShowMetadata(!showMetadata)}
>
{/* // todo: change icon */}
<RiLayoutRight2Line className={cn('w-4 h-4', showMetadata ? 'text-components-button-secondary-accent-text' : 'text-components-button-secondary-text')} />
</button>
</div>
@@ -202,7 +203,7 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
<div className='flex flex-row flex-1' style={{ height: 'calc(100% - 4rem)' }}>
{isDetailLoading
? <Loading type='app' />
: <div className={`h-full w-full flex flex-col ${embedding ? 'px-6 py-3 sm:py-12 sm:px-16' : 'pb-[30px] pt-3 px-6'}`}>
: <div className={`h-full w-full flex flex-col ${embedding ? 'px-6 py-3 sm:py-12 sm:px-16' : 'pb-[30px] pt-3 pl-5 pr-11'}`}>
{embedding
? <Embedding detail={documentDetail} detailUpdate={detailMutate} />
: <Completed
@@ -211,6 +212,8 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
onNewSegmentModalChange={setNewSegmentModalVisible}
importStatus={importStatus}
archived={documentDetail?.archived}
mode={documentDetail?.dataset_process_rule.mode}
parentMode={documentDetail?.dataset_process_rule.rules.parent_mode}
/>
}
</div>

View File

@@ -270,14 +270,14 @@ export const OperationAction: FC<{
popupClassName='text-text-secondary system-xs-medium'
needsDelay
>
<div
<button
className={cn('rounded-lg mr-2 cursor-pointer',
!isListScene
? 'p-2 bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border hover:border-components-button-secondary-border-hover shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]'
: 'p-0.5 hover:bg-state-base-hover')}
onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
<RiEqualizer2Line className='w-4 h-4 text-components-button-secondary-text' />
</div>
</button>
</Tooltip>
<Popover
htmlContent={

View File

@@ -332,6 +332,7 @@ const translation = {
},
segment: {
paragraphs: 'Paragraphs',
chunks: 'CHUNKS',
keywords: 'Key Words',
addKeyWord: 'Add key word',
keywordError: 'The maximum length of keyword is 20',

View File

@@ -330,6 +330,7 @@ const translation = {
},
segment: {
paragraphs: '段落',
chunks: '段落',
keywords: '关键词',
addKeyWord: '添加关键词',
keywordError: '关键词最大长度为 20',

View File

@@ -182,7 +182,7 @@ export type ProcessRuleResponse = {
export type Rules = {
pre_processing_rules: PreProcessingRule[]
segmentation: Segmentation
parent_node: ParentMode
parent_mode: ParentMode
subchunk_segmentation: Segmentation
}
@@ -405,12 +405,12 @@ export const SEGMENT_STATUS_LIST = ['waiting', 'completed', 'error', 'indexing']
export type SegmentStatus = typeof SEGMENT_STATUS_LIST[number]
export type SegmentsQuery = {
last_id?: string
page?: string
limit: number
// status?: SegmentStatus
hit_count_gte?: number
keyword?: string
enabled?: boolean
enabled?: boolean | 'all'
}
export type SegmentDetailModel = {
@@ -435,6 +435,7 @@ export type SegmentDetailModel = {
error: string | null
stopped_at: number
answer?: string
child_chunks?: ChildChunkDetail[]
}
export type SegmentsResponse = {
@@ -442,6 +443,7 @@ export type SegmentsResponse = {
has_more: boolean
limit: number
total: number
total_pages: number
}
export type HitTestingRecord = {
@@ -585,3 +587,15 @@ export const DEFAULT_WEIGHTED_SCORE = {
keyword: 0.3,
},
}
export type ChildChunkType = 'automatic' | 'customized'
export type ChildChunkDetail = {
id: number
position: number
segment_id: string
content: string
word_count: number
created_at: number
type: ChildChunkType
}

View File

@@ -1,16 +1,44 @@
import { useMutation } from '@tanstack/react-query'
import { del, patch } from '../base'
import { useMutation, useQuery } from '@tanstack/react-query'
import { del, get, patch } from '../base'
import type { CommonResponse } from '@/models/common'
import type { SegmentsResponse } from '@/models/datasets'
const NAME_SPACE = 'segment'
const useSegmentListKey = [NAME_SPACE, 'list']
export const useSegmentList = (
payload: {
datasetId: string
documentId: string
params: {
page: number
limit: number
keyword: string
enabled: boolean | 'all'
}
},
disable?: boolean,
) => {
const { datasetId, documentId, params } = payload
const { page, limit, keyword, enabled } = params
return useQuery<SegmentsResponse>({
queryKey: [...useSegmentListKey, datasetId, documentId, page, limit, keyword, enabled],
queryFn: () => {
return get<SegmentsResponse>(`/datasets/${datasetId}/documents/${documentId}/segments`, { params })
},
enabled: !disable,
initialData: disable ? { data: [], has_more: false, total: 0, total_pages: 0, limit: 10 } : undefined,
})
}
export const useEnableSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'enable'],
mutationFn: (payload: { datasetId: string; segmentIds: string[] }) => {
const { datasetId, segmentIds } = payload
mutationFn: (payload: { datasetId: string; documentId: string; segmentIds: string[] }) => {
const { datasetId, documentId, segmentIds } = payload
const query = segmentIds.map(id => `segment_id=${id}`).join('&')
return patch<CommonResponse>(`/datasets/${datasetId}/segments/enable?${query}`)
return patch<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/segments/enable?${query}`)
},
})
}
@@ -18,10 +46,10 @@ export const useEnableSegment = () => {
export const useDisableSegment = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'disable'],
mutationFn: (payload: { datasetId: string; segmentIds: string[] }) => {
const { datasetId, segmentIds } = payload
mutationFn: (payload: { datasetId: string; documentId: string; segmentIds: string[] }) => {
const { datasetId, documentId, segmentIds } = payload
const query = segmentIds.map(id => `segment_id=${id}`).join('&')
return patch<CommonResponse>(`/datasets/${datasetId}/segments/disable?${query}`)
return patch<CommonResponse>(`/datasets/${datasetId}/documents/${documentId}/segments/disable?${query}`)
},
})
}

View File

@@ -99,6 +99,10 @@ module.exports = {
'workflow-process-bg': 'var(--color-workflow-process-bg)',
'dataset-chunk-process-success-bg': 'var(--color-dataset-chunk-process-success-bg)',
'dataset-chunk-process-error-bg': 'var(--color-dataset-chunk-process-error-bg)',
'dataset-chunk-detail-card-hover-bg': 'var(--color-dataset-chunk-detail-card-hover-bg)',
},
lineClamp: {
20: '20',
},
},
},

View File

@@ -4,4 +4,5 @@ html[data-theme="dark"] {
--color-workflow-process-bg: linear-gradient(90deg, rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.04) 100%);
--color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%);
--color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%);
--color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #1D1D20 0%, #222225 100%);
}

View File

@@ -4,4 +4,5 @@ html[data-theme="light"] {
--color-workflow-process-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%);
--color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%);
--color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%);
--color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #F2F4F7 0%, #F9FAFB 100%);
}