From d6cfb2199268ad23ab5dcec75f5da71cbbe8732d Mon Sep 17 00:00:00 2001 From: Mminamiyama Date: Wed, 9 Jul 2025 10:05:31 +0800 Subject: [PATCH] feat(question-classifier): add drag-and-drop sorting for topics list - Implement topic sorting functionality using react-sortablejs - Add draggable handle and visual feedback during sorting - Update node internals after sorting to ensure proper rendering --- .../components/class-list.tsx | 56 +++- .../nodes/question-classifier/panel.tsx | 290 +++++++++--------- .../nodes/question-classifier/use-config.ts | 16 +- 3 files changed, 204 insertions(+), 158 deletions(-) diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index f152917ed4..083e326cdf 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useCallback } from 'react' +import React, { useCallback, useState } from 'react' import produce from 'immer' import { useTranslation } from 'react-i18next' import { useEdgesInteractions } from '../../../hooks' @@ -8,6 +8,10 @@ import AddButton from '../../_base/components/add-button' import Item from './class-item' import type { Topic } from '@/app/components/workflow/nodes/question-classifier/types' import type { ValueSelector, Var } from '@/app/components/workflow/types' +import { ReactSortable } from 'react-sortablejs' +import { noop } from 'lodash-es' +import { RiDraggable } from '@remixicon/react' +import cn from '@/utils/classnames' const i18nPrefix = 'workflow.nodes.questionClassifiers' @@ -17,6 +21,7 @@ type Props = { onChange: (list: Topic[]) => void readonly?: boolean filterVar: (payload: Var, valueSelector: ValueSelector) => boolean + handleSortTopic?: (newTopics: (Topic & { id: string })[]) => void } const ClassList: FC = ({ @@ -25,6 +30,7 @@ const ClassList: FC = ({ onChange, readonly, filterVar, + handleSortTopic = noop, }) => { const { t } = useTranslation() const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions() @@ -55,22 +61,46 @@ const ClassList: FC = ({ } }, [list, onChange, handleEdgeDeleteByDeleteBranch, nodeId]) + const [willDeleteCaseId, setWillDeleteCaseId] = useState('') + const topicCount = list.length + const handleSideWidth = 3 // Todo Remove; edit topic name return ( -
+ ({ ...item }))} + setList={handleSortTopic} + handle='.handle' + ghostClass='bg-components-panel-bg' + animation={150} + disabled={readonly} + className='space-y-2' + > { list.map((item, index) => { return ( - +
+
) }) } @@ -81,7 +111,7 @@ const ClassList: FC = ({ /> )} -
+ ) } export default React.memo(ClassList) diff --git a/web/app/components/workflow/nodes/question-classifier/panel.tsx b/web/app/components/workflow/nodes/question-classifier/panel.tsx index 8cf9ec5f7c..299cfdcf1b 100644 --- a/web/app/components/workflow/nodes/question-classifier/panel.tsx +++ b/web/app/components/workflow/nodes/question-classifier/panel.tsx @@ -1,144 +1,146 @@ -import type { FC } from 'react' -import React from 'react' -import { useTranslation } from 'react-i18next' -import VarReferencePicker from '../_base/components/variable/var-reference-picker' -import ConfigVision from '../_base/components/config-vision' -import useConfig from './use-config' -import ClassList from './components/class-list' -import AdvancedSetting from './components/advanced-setting' -import type { QuestionClassifierNodeType } from './types' -import Field from '@/app/components/workflow/nodes/_base/components/field' -import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' -import type { NodePanelProps } from '@/app/components/workflow/types' -import Split from '@/app/components/workflow/nodes/_base/components/split' -import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' -import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' - -const i18nPrefix = 'workflow.nodes.questionClassifiers' - -const Panel: FC> = ({ - id, - data, -}) => { - const { t } = useTranslation() - - const { - readOnly, - inputs, - handleModelChanged, - isChatMode, - isChatModel, - handleCompletionParamsChange, - handleQueryVarChange, - handleTopicsChange, - hasSetBlockStatus, - availableVars, - availableNodesWithParent, - handleInstructionChange, - handleMemoryChange, - isVisionModel, - handleVisionResolutionChange, - handleVisionResolutionEnabledChange, - filterVar, - } = useConfig(id, data) - - const model = inputs.model - - return ( -
-
- - - - - - - - - - - - -
- - - - -
- - <> - - - - -
-
- ) -} - -export default React.memo(Panel) +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' +import VarReferencePicker from '../_base/components/variable/var-reference-picker' +import ConfigVision from '../_base/components/config-vision' +import useConfig from './use-config' +import ClassList from './components/class-list' +import AdvancedSetting from './components/advanced-setting' +import type { QuestionClassifierNodeType } from './types' +import Field from '@/app/components/workflow/nodes/_base/components/field' +import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' +import type { NodePanelProps } from '@/app/components/workflow/types' +import Split from '@/app/components/workflow/nodes/_base/components/split' +import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' +import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse' + +const i18nPrefix = 'workflow.nodes.questionClassifiers' + +const Panel: FC> = ({ + id, + data, +}) => { + const { t } = useTranslation() + + const { + readOnly, + inputs, + handleModelChanged, + isChatMode, + isChatModel, + handleCompletionParamsChange, + handleQueryVarChange, + handleTopicsChange, + hasSetBlockStatus, + availableVars, + availableNodesWithParent, + handleInstructionChange, + handleMemoryChange, + isVisionModel, + handleVisionResolutionChange, + handleVisionResolutionEnabledChange, + filterVar, + handleSortTopic, + } = useConfig(id, data) + + const model = inputs.model + + return ( +
+
+ + + + + + + + + + + + +
+ + + + +
+ + <> + + + + +
+
+ ) +} + +export default React.memo(Panel) diff --git a/web/app/components/workflow/nodes/question-classifier/use-config.ts b/web/app/components/workflow/nodes/question-classifier/use-config.ts index 8eacf5b43f..a4acf5b7f6 100644 --- a/web/app/components/workflow/nodes/question-classifier/use-config.ts +++ b/web/app/components/workflow/nodes/question-classifier/use-config.ts @@ -9,13 +9,15 @@ import { import { useStore } from '../../store' import useAvailableVarList from '../_base/hooks/use-available-var-list' import useConfigVision from '../../hooks/use-config-vision' -import type { QuestionClassifierNodeType } from './types' +import type { QuestionClassifierNodeType, Topic } from './types' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants' +import { useUpdateNodeInternals } from 'reactflow' const useConfig = (id: string, payload: QuestionClassifierNodeType) => { + const updateNodeInternals = useUpdateNodeInternals() const { nodesReadOnly: readOnly } = useNodesReadOnly() const isChatMode = useIsChatMode() const defaultConfig = useStore(s => s.nodesDefaultConfigs)[payload.type] @@ -166,6 +168,17 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { return varPayload.type === VarType.string }, []) + const handleSortTopic = useCallback((newTopics: (Topic & { id: string })[]) => { + const newInputs = produce(inputs, (draft) => { + draft.classes = newTopics.filter(Boolean).map(item => ({ + id: item.id, + name: item.name, + })) + }) + setInputs(newInputs) + updateNodeInternals(id) + }, [id, inputs, setInputs, updateNodeInternals]) + return { readOnly, inputs, @@ -185,6 +198,7 @@ const useConfig = (id: string, payload: QuestionClassifierNodeType) => { isVisionModel, handleVisionResolutionEnabledChange, handleVisionResolutionChange, + handleSortTopic, } }