From d841581679d1f7cef8855b6068dfb629a107e10a Mon Sep 17 00:00:00 2001 From: twwu Date: Tue, 15 Apr 2025 17:30:18 +0800 Subject: [PATCH 001/295] feat: add IndeterminateIcon component and update Checkbox to support indeterminate state refactor: remove mixed state handling and update related styles fix: update useCallback dependencies for better performance --- .../checkbox/assets/indeterminate-icon.tsx | 9 +++ .../components/base/checkbox/assets/mixed.svg | 5 -- .../components/base/checkbox/index.module.css | 10 ---- web/app/components/base/checkbox/index.tsx | 23 +++++--- .../documents/detail/completed/index.tsx | 55 +++++++++---------- .../detail/completed/segment-card/index.tsx | 6 +- .../detail/completed/segment-detail.tsx | 9 +-- .../detail/completed/segment-list.tsx | 2 +- .../components/datasets/documents/list.tsx | 6 +- 9 files changed, 59 insertions(+), 66 deletions(-) create mode 100644 web/app/components/base/checkbox/assets/indeterminate-icon.tsx delete mode 100644 web/app/components/base/checkbox/assets/mixed.svg delete mode 100644 web/app/components/base/checkbox/index.module.css diff --git a/web/app/components/base/checkbox/assets/indeterminate-icon.tsx b/web/app/components/base/checkbox/assets/indeterminate-icon.tsx new file mode 100644 index 0000000000..7c8c609e08 --- /dev/null +++ b/web/app/components/base/checkbox/assets/indeterminate-icon.tsx @@ -0,0 +1,9 @@ +const IndeterminateIcon = () => { + return ( + + + + ) +} + +export default IndeterminateIcon diff --git a/web/app/components/base/checkbox/assets/mixed.svg b/web/app/components/base/checkbox/assets/mixed.svg deleted file mode 100644 index e16b8fc975..0000000000 --- a/web/app/components/base/checkbox/assets/mixed.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/web/app/components/base/checkbox/index.module.css b/web/app/components/base/checkbox/index.module.css deleted file mode 100644 index d675607b46..0000000000 --- a/web/app/components/base/checkbox/index.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.mixed { - background: var(--color-components-checkbox-bg) url(./assets/mixed.svg) center center no-repeat; - background-size: 12px 12px; - border: none; -} - -.checked.disabled { - background-color: #d0d5dd; - border-color: #d0d5dd; -} \ No newline at end of file diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx index b0b0ebca7c..7abe54ee38 100644 --- a/web/app/components/base/checkbox/index.tsx +++ b/web/app/components/base/checkbox/index.tsx @@ -1,23 +1,25 @@ import { RiCheckLine } from '@remixicon/react' -import s from './index.module.css' import cn from '@/utils/classnames' +import IndeterminateIcon from './assets/indeterminate-icon' type CheckboxProps = { + id?: string checked?: boolean onCheck?: () => void className?: string disabled?: boolean - mixed?: boolean + indeterminate?: boolean } -const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProps) => { +const Checkbox = ({ id, checked, onCheck, className, disabled, indeterminate }: CheckboxProps) => { if (!checked) { return (
{ @@ -25,11 +27,16 @@ const Checkbox = ({ checked, onCheck, className, disabled, mixed }: CheckboxProp return onCheck?.() }} - >
+ > + {indeterminate && ( + + )} + ) } return (
- +
) } diff --git a/web/app/components/datasets/documents/detail/completed/index.tsx b/web/app/components/datasets/documents/detail/completed/index.tsx index 197a6514da..4fe0df2acd 100644 --- a/web/app/components/datasets/documents/detail/completed/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/index.tsx @@ -220,13 +220,11 @@ const Completed: FC = ({ const resetList = useCallback(() => { setSelectedSegmentIds([]) invalidSegmentList() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [invalidSegmentList]) const resetChildList = useCallback(() => { invalidChildSegmentList() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [invalidChildSegmentList]) const onClickCard = (detail: SegmentDetailModel, isEditMode = false) => { setCurrSegment({ segInfo: detail, showModal: true, isEditMode }) @@ -253,7 +251,7 @@ const Completed: FC = ({ const invalidChunkListEnabled = useInvalid(useChunkListEnabledKey) const invalidChunkListDisabled = useInvalid(useChunkListDisabledKey) - const refreshChunkListWithStatusChanged = () => { + const refreshChunkListWithStatusChanged = useCallback(() => { switch (selectedStatus) { case 'all': invalidChunkListDisabled() @@ -262,7 +260,7 @@ const Completed: FC = ({ default: invalidSegmentList() } - } + }, [selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidSegmentList]) const onChangeSwitch = useCallback(async (enable: boolean, segId?: string) => { const operationApi = enable ? enableSegment : disableSegment @@ -280,8 +278,7 @@ const Completed: FC = ({ notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }, }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [datasetId, documentId, selectedSegmentIds, segments]) + }, [datasetId, documentId, selectedSegmentIds, segments, disableSegment, enableSegment, t, notify, refreshChunkListWithStatusChanged]) const { mutateAsync: deleteSegment } = useDeleteSegment() @@ -296,12 +293,11 @@ const Completed: FC = ({ notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }, }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [datasetId, documentId, selectedSegmentIds]) + }, [datasetId, documentId, selectedSegmentIds, deleteSegment, resetList, t, notify]) const { mutateAsync: updateSegment } = useUpdateSegment() - const refreshChunkListDataWithDetailChanged = () => { + const refreshChunkListDataWithDetailChanged = useCallback(() => { switch (selectedStatus) { case 'all': invalidChunkListDisabled() @@ -316,7 +312,7 @@ const Completed: FC = ({ invalidChunkListEnabled() break } - } + }, [selectedStatus, invalidChunkListDisabled, invalidChunkListEnabled, invalidChunkListAll]) const handleUpdateSegment = useCallback(async ( segmentId: string, @@ -375,17 +371,18 @@ const Completed: FC = ({ eventEmitter?.emit('update-segment-done') }, }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [segments, datasetId, documentId]) + }, [segments, datasetId, documentId, updateSegment, docForm, notify, eventEmitter, onCloseSegmentDetail, refreshChunkListDataWithDetailChanged, t]) useEffect(() => { resetList() + // eslint-disable-next-line react-hooks/exhaustive-deps }, [pathname]) useEffect(() => { if (importStatus === ProcessStatus.COMPLETED) resetList() - }, [importStatus, resetList]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [importStatus]) const onCancelBatchOperation = useCallback(() => { setSelectedSegmentIds([]) @@ -430,8 +427,7 @@ const Completed: FC = ({ const count = segmentListData?.total || 0 return `${total} ${t('datasetDocuments.segment.searchResults', { count })}` } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [segmentListData?.total, mode, parentMode, searchValue, selectedStatus]) + }, [segmentListData, mode, parentMode, searchValue, selectedStatus, t]) const toggleFullScreen = useCallback(() => { setFullScreen(!fullScreen) @@ -449,8 +445,7 @@ const Completed: FC = ({ resetList() currentPage !== totalPages && setCurrentPage(totalPages) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [segmentListData, limit, currentPage]) + }, [segmentListData, limit, currentPage, resetList]) const { mutateAsync: deleteChildSegment } = useDeleteChildSegment() @@ -470,8 +465,7 @@ const Completed: FC = ({ }, }, ) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [datasetId, documentId, parentMode]) + }, [datasetId, documentId, parentMode, deleteChildSegment, resetList, resetChildList, t, notify]) const handleAddNewChildChunk = useCallback((parentChunkId: string) => { setShowNewChildSegmentModal(true) @@ -490,8 +484,7 @@ const Completed: FC = ({ else { resetChildList() } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [parentMode, currChunkId, segments]) + }, [parentMode, currChunkId, segments, refreshChunkListDataWithDetailChanged, resetChildList]) const viewNewlyAddedChildChunk = useCallback(() => { const totalPages = childChunkListData?.total_pages || 0 @@ -505,8 +498,7 @@ const Completed: FC = ({ resetChildList() currentPage !== totalPages && setCurrentPage(totalPages) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [childChunkListData, limit, currentPage]) + }, [childChunkListData, limit, currentPage, resetChildList]) const onClickSlice = useCallback((detail: ChildChunkDetail) => { setCurrChildChunk({ childChunkInfo: detail, showModal: true }) @@ -560,8 +552,7 @@ const Completed: FC = ({ eventEmitter?.emit('update-child-segment-done') }, }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [segments, childSegments, datasetId, documentId, parentMode]) + }, [segments, datasetId, documentId, parentMode, updateChildSegment, notify, eventEmitter, onCloseChildSegmentDetail, refreshChunkListDataWithDetailChanged, resetChildList, t]) const onClearFilter = useCallback(() => { setInputValue('') @@ -570,6 +561,12 @@ const Completed: FC = ({ setCurrentPage(1) }, []) + const selectDefaultValue = useMemo(() => { + if (selectedStatus === 'all') + return 'all' + return selectedStatus ? 1 : 0 + }, [selectedStatus]) + return ( = ({ @@ -591,7 +588,7 @@ const Completed: FC = ({ = ({ const wordCountText = useMemo(() => { const total = formatNumber(word_count) return `${total} ${t('datasetDocuments.segment.characters', { count: word_count })}` - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [word_count]) + }, [word_count, t]) const labelPrefix = useMemo(() => { return isParentChildMode ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk') - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isParentChildMode]) + }, [isParentChildMode, t]) if (loading) return diff --git a/web/app/components/datasets/documents/detail/completed/segment-detail.tsx b/web/app/components/datasets/documents/detail/completed/segment-detail.tsx index cea3402499..d3575c18ed 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-detail.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-detail.tsx @@ -86,8 +86,7 @@ const SegmentDetail: FC = ({ const titleText = useMemo(() => { return isEditMode ? t('datasetDocuments.segment.editChunk') : t('datasetDocuments.segment.chunkDetail') - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isEditMode]) + }, [isEditMode, t]) const isQAModel = useMemo(() => { return docForm === ChunkingMode.qa @@ -98,13 +97,11 @@ const SegmentDetail: FC = ({ const total = formatNumber(isEditMode ? contentLength : segInfo!.word_count as number) const count = isEditMode ? contentLength : segInfo!.word_count as number return `${total} ${t('datasetDocuments.segment.characters', { count })}` - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isEditMode, question.length, answer.length, segInfo?.word_count, isQAModel]) + }, [isEditMode, question.length, answer.length, isQAModel, segInfo, t]) const labelPrefix = useMemo(() => { return isParentChildMode ? t('datasetDocuments.segment.parentChunk') : t('datasetDocuments.segment.chunk') - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isParentChildMode]) + }, [isParentChildMode, t]) return (
diff --git a/web/app/components/datasets/documents/detail/completed/segment-list.tsx b/web/app/components/datasets/documents/detail/completed/segment-list.tsx index b2351c1b97..f6076e5813 100644 --- a/web/app/components/datasets/documents/detail/completed/segment-list.tsx +++ b/web/app/components/datasets/documents/detail/completed/segment-list.tsx @@ -42,7 +42,7 @@ const SegmentList = ( embeddingAvailable, onClearFilter, }: ISegmentListProps & { - ref: React.RefObject; + ref: React.LegacyRef }, ) => { const mode = useDocumentContext(s => s.mode) diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx index 8ed878fe56..cb349ee01c 100644 --- a/web/app/components/datasets/documents/list.tsx +++ b/web/app/components/datasets/documents/list.tsx @@ -202,7 +202,7 @@ export const OperationAction: FC<{ const isListScene = scene === 'list' const onOperate = async (operationName: OperationName) => { - let opApi = deleteDocument + let opApi switch (operationName) { case 'archive': opApi = archiveDocument @@ -490,7 +490,7 @@ const DocumentList: FC = ({ const handleAction = (actionName: DocumentActionType) => { return async () => { - let opApi = deleteDocument + let opApi switch (actionName) { case DocumentActionType.archive: opApi = archiveDocument @@ -527,7 +527,7 @@ const DocumentList: FC = ({ )} From 942648e9e9659baa4c4d84778493a4c725708ebd Mon Sep 17 00:00:00 2001 From: twwu Date: Wed, 16 Apr 2025 14:16:32 +0800 Subject: [PATCH 002/295] feat: implement form components including CheckboxField, SelectField, TextField, and SubmitButton with validation --- web/app/components/base/checkbox/index.tsx | 47 ++++++------- .../base/form/components/checkbox-field.tsx | 43 ++++++++++++ .../components/base/form/components/label.tsx | 44 ++++++++++++ .../base/form/components/select-filed.tsx | 42 ++++++++++++ .../base/form/components/submit-button.tsx | 25 +++++++ .../base/form/components/text-field.tsx | 37 ++++++++++ .../form-scenarios/demo/contact-fields.tsx | 35 ++++++++++ .../base/form/form-scenarios/demo/index.tsx | 68 +++++++++++++++++++ .../form-scenarios/demo/shared-options.tsx | 14 ++++ .../base/form/form-scenarios/demo/types.ts | 34 ++++++++++ web/app/components/base/form/index.tsx | 21 ++++++ web/package.json | 1 + web/pnpm-lock.yaml | 60 ++++++++++++++++ 13 files changed, 444 insertions(+), 27 deletions(-) create mode 100644 web/app/components/base/form/components/checkbox-field.tsx create mode 100644 web/app/components/base/form/components/label.tsx create mode 100644 web/app/components/base/form/components/select-filed.tsx create mode 100644 web/app/components/base/form/components/submit-button.tsx create mode 100644 web/app/components/base/form/components/text-field.tsx create mode 100644 web/app/components/base/form/form-scenarios/demo/contact-fields.tsx create mode 100644 web/app/components/base/form/form-scenarios/demo/index.tsx create mode 100644 web/app/components/base/form/form-scenarios/demo/shared-options.tsx create mode 100644 web/app/components/base/form/form-scenarios/demo/types.ts create mode 100644 web/app/components/base/form/index.tsx diff --git a/web/app/components/base/checkbox/index.tsx b/web/app/components/base/checkbox/index.tsx index 7abe54ee38..99a31234f7 100644 --- a/web/app/components/base/checkbox/index.tsx +++ b/web/app/components/base/checkbox/index.tsx @@ -11,45 +11,38 @@ type CheckboxProps = { indeterminate?: boolean } -const Checkbox = ({ id, checked, onCheck, className, disabled, indeterminate }: CheckboxProps) => { - if (!checked) { - return ( -
{ - if (disabled) - return - onCheck?.() - }} - > - {indeterminate && ( - - )} -
- ) - } +const Checkbox = ({ + id, + checked, + onCheck, + className, + disabled, + indeterminate, +}: CheckboxProps) => { + const checkClassName = (checked || indeterminate) + ? 'bg-components-checkbox-bg text-components-checkbox-icon hover:bg-components-checkbox-bg-hover' + : 'border border-components-checkbox-border bg-components-checkbox-bg-unchecked hover:bg-components-checkbox-bg-unchecked-hover hover:border-components-checkbox-border-hover' + const disabledClassName = (checked || indeterminate) + ? 'cursor-not-allowed bg-components-checkbox-bg-disabled-checked text-components-checkbox-icon-disabled hover:bg-components-checkbox-bg-disabled-checked' + : 'cursor-not-allowed border-components-checkbox-border-disabled bg-components-checkbox-bg-disabled hover:border-components-checkbox-border-disabled hover:bg-components-checkbox-bg-disabled' + return (
{ if (disabled) return - onCheck?.() }} > - + {!checked && indeterminate && } + {checked && }
) } diff --git a/web/app/components/base/form/components/checkbox-field.tsx b/web/app/components/base/form/components/checkbox-field.tsx new file mode 100644 index 0000000000..ec0cb5d76f --- /dev/null +++ b/web/app/components/base/form/components/checkbox-field.tsx @@ -0,0 +1,43 @@ +import cn from '@/utils/classnames' +import { useFieldContext } from '..' +import Checkbox from '../../checkbox' + +type CheckboxFieldProps = { + label: string; + labelClassName?: string; +} + +const CheckboxField = ({ + label, + labelClassName, +}: CheckboxFieldProps) => { + const field = useFieldContext() + + return ( +
+
+ { + field.handleChange(!field.state.value) + }} + /> +
+ +
+ ) +} + +export default CheckboxField diff --git a/web/app/components/base/form/components/label.tsx b/web/app/components/base/form/components/label.tsx new file mode 100644 index 0000000000..1e06d256e7 --- /dev/null +++ b/web/app/components/base/form/components/label.tsx @@ -0,0 +1,44 @@ +import cn from '@/utils/classnames' +import Tooltip from '../../tooltip' + +type LabelProps = { + htmlFor: string + label: string + isRequired?: boolean + showOptional?: boolean + tooltip?: string + className?: string + labelClassName?: string +} + +const Label = ({ + htmlFor, + label, + isRequired, + showOptional, + tooltip, + labelClassName, +}: LabelProps) => { + return ( +
+ + {showOptional &&
(Optional)
} + {isRequired &&
*
} + {tooltip && ( + {tooltip}
+ } + triggerClassName='ml-0.5 w-4 h-4' + /> + )} +
+ ) +} + +export default Label diff --git a/web/app/components/base/form/components/select-filed.tsx b/web/app/components/base/form/components/select-filed.tsx new file mode 100644 index 0000000000..eab9a79b2b --- /dev/null +++ b/web/app/components/base/form/components/select-filed.tsx @@ -0,0 +1,42 @@ +import cn from '@/utils/classnames' +import { useFieldContext } from '..' +import PureSelect from '../../select/pure' +import Label from './label' + +type SelectOption = { + value: string + label: string +} + +type SelectFieldProps = { + label: string + options: SelectOption[] + className?: string + labelClassName?: string +} + +const SelectField = ({ + label, + options, + className, + labelClassName, +}: SelectFieldProps) => { + const field = useFieldContext() + + return ( +
+
+ ) +} + +export default SelectField diff --git a/web/app/components/base/form/components/submit-button.tsx b/web/app/components/base/form/components/submit-button.tsx new file mode 100644 index 0000000000..f5b891f22f --- /dev/null +++ b/web/app/components/base/form/components/submit-button.tsx @@ -0,0 +1,25 @@ +import { useStore } from '@tanstack/react-form' +import { useFormContext } from '..' +import Button, { type ButtonProps } from '../../button' + +type SubmitButtonProps = Omit + +const SubmitButton = ({ ...buttonProps }: SubmitButtonProps) => { + const form = useFormContext() + + const [isSubmitting, canSubmit] = useStore(form.store, state => [ + state.isSubmitting, + state.canSubmit, + ]) + + return ( + + diff --git a/web/app/components/base/input/index.tsx b/web/app/components/base/input/index.tsx index 5f059c3b7f..30fd90aff8 100644 --- a/web/app/components/base/input/index.tsx +++ b/web/app/components/base/input/index.tsx @@ -30,7 +30,7 @@ export type InputProps = { wrapperClassName?: string styleCss?: CSSProperties unit?: string -} & React.InputHTMLAttributes & VariantProps +} & Omit, 'size'> & VariantProps const Input = ({ size, diff --git a/web/app/components/base/param-item/index.tsx b/web/app/components/base/param-item/index.tsx index 4cae402e3b..03eb5a7c42 100644 --- a/web/app/components/base/param-item/index.tsx +++ b/web/app/components/base/param-item/index.tsx @@ -54,7 +54,7 @@ const ParamItem: FC = ({ className, id, name, noTooltip, tip, step = 0.1, max={max} step={step} amount={step} - size='sm' + size='regular' value={value} onChange={(value) => { onChange(id, value) diff --git a/web/app/components/datasets/create/step-two/inputs.tsx b/web/app/components/datasets/create/step-two/inputs.tsx index 0b4345d7b4..d6dc6e9d56 100644 --- a/web/app/components/datasets/create/step-two/inputs.tsx +++ b/web/app/components/datasets/create/step-two/inputs.tsx @@ -47,7 +47,7 @@ export const MaxLengthInput: FC = (props) => { }> = (props) => { }> = ({ className={cn(className, 'rounded-l-md')} value={value} onChange={onChange} - size='sm' + size='regular' controlWrapClassName='overflow-hidden' controlClassName='pt-0 pb-0' readOnly={readOnly} diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index 63f9fb92ec..434f4bfdf4 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -133,7 +133,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { // TODO: maybe empty, handle this onChange={onChange as any} defaultValue={defaultValue} - size='sm' + size='regular' min={def.min} max={def.max} className='w-12' From 6eef5990c9524435067b3b2cbdcdfdab1aad9da4 Mon Sep 17 00:00:00 2001 From: twwu Date: Fri, 18 Apr 2025 11:32:23 +0800 Subject: [PATCH 005/295] feat: enhance form components with additional props for validation and tooltips; add OptionsField component --- .../form/components/field/number-input.tsx | 16 +++++++-- .../base/form/components/field/options.tsx | 34 +++++++++++++++++++ .../base/form/components/field/select.tsx | 11 +++++- .../base/form/components/field/text.tsx | 17 ++++++++-- .../components/base/form/components/label.tsx | 12 ++++--- web/app/components/base/form/index.tsx | 2 ++ 6 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 web/app/components/base/form/components/field/options.tsx diff --git a/web/app/components/base/form/components/field/number-input.tsx b/web/app/components/base/form/components/field/number-input.tsx index 681231a4fe..fce3143fe1 100644 --- a/web/app/components/base/form/components/field/number-input.tsx +++ b/web/app/components/base/form/components/field/number-input.tsx @@ -2,18 +2,26 @@ import React from 'react' import { useFieldContext } from '../..' import Label from '../label' import cn from '@/utils/classnames' +import type { InputNumberProps } from '../../../input-number' import { InputNumber } from '../../../input-number' type TextFieldProps = { label: string + isRequired?: boolean + showOptional?: boolean + tooltip?: string className?: string labelClassName?: string -} +} & Omit const NumberInputField = ({ label, + isRequired, + showOptional, + tooltip, className, labelClassName, + ...inputProps }: TextFieldProps) => { const field = useFieldContext() @@ -22,13 +30,17 @@ const NumberInputField = ({