diff --git a/web/app/components/base/checkbox/assets/mixed.svg b/web/app/components/base/checkbox/assets/mixed.svg new file mode 100644 index 0000000000..e16b8fc975 --- /dev/null +++ b/web/app/components/base/checkbox/assets/mixed.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/checkbox/index.module.css b/web/app/components/base/checkbox/index.module.css index 5fe4172f13..30c887801b 100644 --- a/web/app/components/base/checkbox/index.module.css +++ b/web/app/components/base/checkbox/index.module.css @@ -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 { diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx index fe95155b3c..51d5fd9027 100644 --- a/web/app/components/base/checkbox/index.tsx +++ b/web/app/components/base/checkbox/index.tsx @@ -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 (
{ diff --git a/web/app/components/base/icons/assets/public/knowledge/chunk.svg b/web/app/components/base/icons/assets/public/knowledge/chunk.svg new file mode 100644 index 0000000000..1dc04943fc --- /dev/null +++ b/web/app/components/base/icons/assets/public/knowledge/chunk.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/web/app/components/base/icons/assets/public/knowledge/collapse.svg b/web/app/components/base/icons/assets/public/knowledge/collapse.svg new file mode 100644 index 0000000000..b54e046085 --- /dev/null +++ b/web/app/components/base/icons/assets/public/knowledge/collapse.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/app/components/base/icons/src/public/knowledge/Chunk.json b/web/app/components/base/icons/src/public/knowledge/Chunk.json new file mode 100644 index 0000000000..469d85d1a7 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/Chunk.json @@ -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" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/knowledge/Chunk.tsx b/web/app/components/base/icons/src/public/knowledge/Chunk.tsx new file mode 100644 index 0000000000..87ff635811 --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/Chunk.tsx @@ -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, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Chunk' + +export default Icon diff --git a/web/app/components/base/icons/src/public/knowledge/Collapse.json b/web/app/components/base/icons/src/public/knowledge/Collapse.json new file mode 100644 index 0000000000..66d457155d --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/Collapse.json @@ -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" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/public/knowledge/Collapse.tsx b/web/app/components/base/icons/src/public/knowledge/Collapse.tsx new file mode 100644 index 0000000000..48206c4d0c --- /dev/null +++ b/web/app/components/base/icons/src/public/knowledge/Collapse.tsx @@ -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, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Collapse' + +export default Icon diff --git a/web/app/components/base/icons/src/public/knowledge/index.ts b/web/app/components/base/icons/src/public/knowledge/index.ts index 24bcf3e41c..86300c9ee3 100644 --- a/web/app/components/base/icons/src/public/knowledge/index.ts +++ b/web/app/components/base/icons/src/public/knowledge/index.ts @@ -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' diff --git a/web/app/components/base/tooltip/index.tsx b/web/app/components/base/tooltip/index.tsx index f3b4cff132..e5f27fdd8c 100644 --- a/web/app/components/base/tooltip/index.tsx +++ b/web/app/components/base/tooltip/index.tsx @@ -96,7 +96,7 @@ const Tooltip: FC = ({ > {popupContent && (
triggerMethod === 'hover' && setHoverPopup()} diff --git a/web/app/components/datasets/documents/detail/completed/batch-action.tsx b/web/app/components/datasets/documents/detail/completed/batch-action.tsx new file mode 100644 index 0000000000..3bed21df96 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/batch-action.tsx @@ -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 + onBatchDisable: () => Promise + onBatchDelete: () => Promise + onCancel: () => void +} + +const BatchAction: FC = ({ + selectedSegmentIds, + onBatchEnable, + onBatchDisable, + onBatchDelete, + onCancel, +}) => { + return ( +
+
+
+ + {selectedSegmentIds.length} + + Selected +
+ +
+ + +
+
+ + +
+
+ + +
+ + +
+
+ ) +} + +export default React.memo(BatchAction) diff --git a/web/app/components/datasets/documents/detail/completed/display-toggle.tsx b/web/app/components/datasets/documents/detail/completed/display-toggle.tsx new file mode 100644 index 0000000000..96a6f5922f --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/display-toggle.tsx @@ -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 ( + + + + + ) +} + +export default React.memo(DisplayToggle) diff --git a/web/app/components/datasets/documents/detail/completed/index.tsx b/web/app/components/datasets/documents/detail/completed/index.tsx index 2c9e6ca2ea..c7978620a3 100644 --- a/web/app/components/datasets/documents/detail/completed/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/index.tsx @@ -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 ( -
- - {localPositionId} +
+ +
+ {localPositionId} +
) } @@ -52,10 +69,11 @@ export const SegmentIndexTag: FC<{ positionId: string | number; className?: stri type ISegmentDetailProps = { embeddingAvailable: boolean segInfo?: Partial & { id: string } - onChangeSwitch?: (segId: string, enabled: boolean) => Promise + onChangeSwitch?: (enabled: boolean, segId?: string) => Promise 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 = ({ 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(segInfo?.keywords || []) @@ -195,7 +214,7 @@ const SegmentDetailComponent: FC = ({ 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 = ({ 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('') // 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 [segments, setSegments] = useState([]) // all segments data + const [selectedSegmentIds, setSelectedSegmentIds] = useState([]) 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 = ({ 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({ + 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) - 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(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 { mutateAsync: enableSegment } = useEnableSegment() - 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 { 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 = ({ 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 ( - <> + setIsCollapsed(!isCollapsed), + }}>
-
{total ? formatNumber(total) : '--'} {t('datasetDocuments.segment.paragraphs')}
+ +
{totalText} {t('datasetDocuments.segment.chunks')}
= ({ ]} defaultValue={'all'} className={s.select} - wrapperClassName='h-fit w-[120px] mr-2' /> + wrapperClassName='h-fit w-[100px] mr-2' /> = ({ onChange={e => handleInputChange(e.target.value)} onClear={() => handleInputChange('')} /> + +
- = ({ = ({ onCancel={() => onNewSegmentModalChange(false)} onSave={resetList} /> - + {selectedSegmentIds.length > 0 + && } +
) } diff --git a/web/app/components/datasets/documents/detail/completed/mock-data.ts b/web/app/components/datasets/documents/detail/completed/mock-data.ts new file mode 100644 index 0000000000..d655b4b2de --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/mock-data.ts @@ -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, +} diff --git a/web/app/components/datasets/documents/detail/completed/segment-card.tsx b/web/app/components/datasets/documents/detail/completed/segment-card.tsx new file mode 100644 index 0000000000..d929c85401 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/segment-card.tsx @@ -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 ( +
·
+ ) +}) + +Dot.displayName = 'Dot' + +const ProgressBar: FC<{ percent: number; loading: boolean }> = React.memo(({ percent, loading }) => { + return ( +
+
+
+
+
{loading ? null : percent.toFixed(2)}
+
+ ) +}) + +ProgressBar.displayName = 'ProgressBar' + +type DocumentTitleProps = { + name: string + extension?: string +} + +const DocumentTitle: FC = React.memo(({ extension, name }) => { + return ( +
+ + {name || '--'} +
+ ) +}) + +DocumentTitle.displayName = 'DocumentTitle' + +const Tag = React.memo(({ text }: { text: string }) => { + return ( +
+ # + {text} +
+ ) +}) + +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 + onDelete?: (segId: string) => Promise + onClickEdit?: () => void + scene?: UsageScene + className?: string + archived?: boolean + embeddingAvailable?: boolean +} + +const SegmentCard: FC = ({ + 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['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 ( + <> +
+
Q
+
{content}
+
+
+
A
+
{answer}
+
+ + ) + } + + if (contentExternal) + return contentExternal + + return content + } + const isCollapsed = useSegmentListContext(s => s.isCollapsed) + + return ( +
onClick?.()}> +
+ {isDocScene + ? <> +
+ + +
{`${formatNumber(word_count)} Characters`}
+ +
{`${formatNumber(hit_count)} Retrieval Count`}
+ + {chunkEdited && ( + + )} +
+
+ {loading + ? ( + + ) + : ( + <> + + {embeddingAvailable && ( +
+ {!archived && ( + <> +
{ + e.stopPropagation() + onClickEdit?.() + }}> + +
+
{ + e.stopPropagation() + setShowModal(true) + } + }> + +
+ + + )} +
) => + e.stopPropagation() + } + className="flex items-center" + > + { + await onChangeSwitch?.(val, id) + }} + /> +
+
+ )} + + )} +
+ + : ( + score !== null + ? ( +
+
+ +
+ ) + : null + )} +
+ {loading + ? ( +
+
+
+ ) + : ( + isDocScene + ? <> +
+ {renderContent()} +
+
+ {keywords?.map(keyword => )} +
+ + : <> +
+ {renderContent()} +
+
+ +
+ +
+ {isExternal ? t('datasetHitTesting.viewDetail') : t('datasetHitTesting.viewChart')} + +
+
+
+ + )} + {showModal + && { await onDelete?.(id) }} + onCancel={() => setShowModal(false)} + /> + } +
+ ) +} + +export default React.memo(SegmentCard) diff --git a/web/app/components/datasets/documents/detail/completed/segment-list.tsx b/web/app/components/datasets/documents/detail/completed/segment-list.tsx new file mode 100644 index 0000000000..6e6c849ab3 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/segment-list.tsx @@ -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 + onDelete: (segId: string) => Promise + archived?: boolean + embeddingAvailable: boolean +} + +const SegmentList: FC = ({ + isLoading, + items, + selectedSegmentIds, + onSelected, + onClick: onClickCard, + onChangeSwitch, + onDelete, + archived, + embeddingAvailable, +}) => { + if (isLoading) + return + return ( +
+ { + items.map((segItem) => { + const isLast = items[items.length - 1].id === segItem.id + return ( +
+ onSelected(segItem.id)} + /> +
+ onClickCard(segItem)} + onChangeSwitch={onChangeSwitch} + onClickEdit={() => onClickCard(segItem, true)} + onDelete={onDelete} + loading={false} + archived={archived} + embeddingAvailable={embeddingAvailable} + /> + {!isLast &&
+ +
} +
+
+ ) + }) + } +
+ ) +} + +export default SegmentList diff --git a/web/app/components/datasets/documents/detail/completed/style.module.css b/web/app/components/datasets/documents/detail/completed/style.module.css index 7633d53209..610d959efe 100644 --- a/web/app/components/datasets/documents/detail/completed/style.module.css +++ b/web/app/components/datasets/documents/detail/completed/style.module.css @@ -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); diff --git a/web/app/components/datasets/documents/detail/index.tsx b/web/app/components/datasets/documents/detail/index.tsx index 5d6e2baba2..ea3a91245f 100644 --- a/web/app/components/datasets/documents/detail/index.tsx +++ b/web/app/components/datasets/documents/detail/index.tsx @@ -195,6 +195,7 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => { className={style.layoutRightIcon} onClick={() => setShowMetadata(!showMetadata)} > + {/* // todo: change icon */}
@@ -202,7 +203,7 @@ const DocumentDetail: FC = ({ datasetId, documentId }) => {
{isDetailLoading ? - :
+ :
{embedding ? : = ({ datasetId, documentId }) => { onNewSegmentModalChange={setNewSegmentModalVisible} importStatus={importStatus} archived={documentDetail?.archived} + mode={documentDetail?.dataset_process_rule.mode} + parentMode={documentDetail?.dataset_process_rule.rules.parent_mode} /> }
diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx index ee883e912b..4321a610cb 100644 --- a/web/app/components/datasets/documents/list.tsx +++ b/web/app/components/datasets/documents/list.tsx @@ -270,14 +270,14 @@ export const OperationAction: FC<{ popupClassName='text-text-secondary system-xs-medium' needsDelay > -
router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}> -
+ { + const { datasetId, documentId, params } = payload + const { page, limit, keyword, enabled } = params + return useQuery({ + queryKey: [...useSegmentListKey, datasetId, documentId, page, limit, keyword, enabled], + queryFn: () => { + return get(`/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(`/datasets/${datasetId}/segments/enable?${query}`) + return patch(`/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(`/datasets/${datasetId}/segments/disable?${query}`) + return patch(`/datasets/${datasetId}/documents/${documentId}/segments/disable?${query}`) }, }) } diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 956cfb66f5..59c896f23f 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -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', }, }, }, diff --git a/web/themes/manual-dark.css b/web/themes/manual-dark.css index 26604370c4..6e2f50849f 100644 --- a/web/themes/manual-dark.css +++ b/web/themes/manual-dark.css @@ -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%); } \ No newline at end of file diff --git a/web/themes/manual-light.css b/web/themes/manual-light.css index 2113ef7fc0..c85dd40a87 100644 --- a/web/themes/manual-light.css +++ b/web/themes/manual-light.css @@ -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%); } \ No newline at end of file