'use client' import type { FC } from 'react' import React, { memo, 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 { RiCloseLine, RiEditLine, } from '@remixicon/react' import { StatusItem } from '../../list' import { DocumentContext } from '../index' import { ProcessStatus } from '../segment-add' import s from './style.module.css' import InfiniteVirtualList from './InfiniteVirtualList' import cn from '@/utils/classnames' import { formatNumber } from '@/utils/format' import Modal from '@/app/components/base/modal' import Switch from '@/app/components/base/switch' import Divider from '@/app/components/base/divider' 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 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' 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') }, [positionId]) return (
{localPositionId}
) } type ISegmentDetailProps = { embeddingAvailable: boolean segInfo?: Partial & { id: string } onChangeSwitch?: (segId: string, enabled: boolean) => Promise onUpdate: (segmentId: string, q: string, a: string, k: string[]) => void onCancel: () => void archived?: boolean } /** * Show all the contents of the segment */ const SegmentDetailComponent: FC = ({ embeddingAvailable, segInfo, archived, onChangeSwitch, onUpdate, onCancel, }) => { const { t } = useTranslation() const [isEditing, setIsEditing] = useState(false) const [question, setQuestion] = useState(segInfo?.content || '') const [answer, setAnswer] = useState(segInfo?.answer || '') const [keywords, setKeywords] = useState(segInfo?.keywords || []) const { eventEmitter } = useEventEmitterContextContext() const [loading, setLoading] = useState(false) eventEmitter?.useSubscription((v) => { if (v === 'update-segment') setLoading(true) else setLoading(false) }) const handleCancel = () => { setIsEditing(false) setQuestion(segInfo?.content || '') setAnswer(segInfo?.answer || '') setKeywords(segInfo?.keywords || []) } const handleSave = () => { onUpdate(segInfo?.id || '', question, answer, keywords) } const renderContent = () => { if (segInfo?.answer) { return ( <>
QUESTION
setQuestion(e.target.value)} disabled={!isEditing} />
ANSWER
setAnswer(e.target.value)} disabled={!isEditing} autoFocus /> ) } return ( setQuestion(e.target.value)} disabled={!isEditing} autoFocus /> ) } return (
{isEditing && ( <> )} {!isEditing && !archived && embeddingAvailable && ( <>
{t('common.operation.edit')}
setIsEditing(true)} />
)}
{renderContent()}
{t('datasetDocuments.segment.keywords')}
{!segInfo?.keywords?.length ? '-' : ( setKeywords(newKeywords)} disableAdd={!isEditing} disableRemove={!isEditing || (keywords.length === 1)} /> ) }
{formatNumber(segInfo?.word_count as number)} {t('datasetDocuments.segment.characters')}
{formatNumber(segInfo?.hit_count as number)} {t('datasetDocuments.segment.hitCount')}
{t('datasetDocuments.segment.vectorHash')}{segInfo?.index_node_hash}
{embeddingAvailable && ( <> { await onChangeSwitch?.(segInfo?.id || '', val) }} disabled={archived} /> )}
) } export const SegmentDetail = memo(SegmentDetailComponent) export const splitArray = (arr: any[], size = 3) => { if (!arr || !arr.length) return [] const result = [] for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size)) return result } type ICompletedProps = { embeddingAvailable: boolean showNewSegmentModal: boolean onNewSegmentModalChange: (state: boolean) => void importStatus: ProcessStatus | string | undefined archived?: boolean // data: Array<{}> // all/part segments } /** * Embedding done, show list of all segments * Support search and filter */ const Completed: FC = ({ embeddingAvailable, showNewSegmentModal, onNewSegmentModalChange, importStatus, archived, }) => { 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 [inputValue, setInputValue] = useState('') // the input value const [searchValue, setSearchValue] = useState('') // the search value const [selectedStatus, setSelectedStatus] = useState('all') // the selected status, enabled/disabled/undefined const [lastSegmentsRes, setLastSegmentsRes] = useState(undefined) const [allSegments, setAllSegments] = useState>([]) // all segments data const [loading, setLoading] = useState(false) const [total, setTotal] = useState() const { eventEmitter } = useEventEmitterContextContext() const { run: handleSearch } = useDebounceFn(() => { setSearchValue(inputValue) }, { wait: 500 }) const handleInputChange = (value: string) => { setInputValue(value) handleSearch() } const onChangeStatus = ({ value }: Item) => { 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(fetchSegments({ datasetId, documentId, params: omitBy({ last_id: !needLastId ? undefined : finalLastId, limit: 12, keyword: searchValue, enabled: selectedStatus === 'all' ? 'all' : !!selectedStatus, }, isNil) as SegmentsQuery, }) as Promise) if (!e) { setAllSegments([...(!needLastId ? [] : allSegments), ...splitArray(res.data || [])]) setLastSegmentsRes(res) if (!lastSegmentsRes || !needLastId) setTotal(res?.total || 0) } setLoading(false) } const resetList = () => { setLastSegmentsRes(undefined) setAllSegments([]) setLoading(false) setTotal(undefined) getSegments(false) } const onClickCard = (detail: SegmentDetailModel) => { setCurrSegment({ segInfo: detail, showModal: true }) } const onCloseModal = () => { setCurrSegment({ ...currSegment, showModal: false }) } const onChangeSwitch = async (segId: string, enabled: boolean) => { const opApi = enabled ? enableSegment : disableSegment const [e] = await asyncRunSafe(opApi({ datasetId, segmentId: segId }) as Promise) 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 onDelete = async (segId: string) => { const [e] = await asyncRunSafe(deleteSegment({ datasetId, documentId, segmentId: segId }) as Promise) if (!e) { notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) resetList() } else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } } const handleUpdateSegment = async (segmentId: string, question: string, answer: string, keywords: string[]) => { const params: SegmentUpdater = { content: '' } if (docForm === 'qa_model') { if (!question.trim()) return notify({ type: 'error', message: t('datasetDocuments.segment.questionEmpty') }) if (!answer.trim()) return notify({ type: 'error', message: t('datasetDocuments.segment.answerEmpty') }) params.content = question params.answer = answer } else { if (!question.trim()) return notify({ type: 'error', message: t('datasetDocuments.segment.contentEmpty') }) params.content = question } if (keywords.length) params.keywords = keywords try { eventEmitter?.emit('update-segment') 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 } } } setAllSegments([...allSegments]) } finally { eventEmitter?.emit('') } } useEffect(() => { if (lastSegmentsRes !== undefined) getSegments(false) }, [selectedStatus, searchValue]) useEffect(() => { if (importStatus === ProcessStatus.COMPLETED) resetList() }, [importStatus]) return ( <>
{total ? formatNumber(total) : '--'} {t('datasetDocuments.segment.paragraphs')}
handleInputChange(e.target.value)} onClear={() => handleInputChange('')} />
{ }} className='!max-w-[640px] !overflow-visible'> onNewSegmentModalChange(false)} onSave={resetList} /> ) } export default Completed