From 2a2193dabb5aa53855d0a42e8915cd5b0c76d491 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:23:28 +0800 Subject: [PATCH] fix(web): correct knowledge dialog draft state (#38794) --- .../persistence/local-storage-bridge.tsx | 14 +- .../knowledge/__tests__/index.spec.tsx | 161 ++-- .../orchestrate/knowledge/dialog.tsx | 871 +++++++++--------- .../orchestrate/prompt-editor/index.tsx | 11 +- 4 files changed, 537 insertions(+), 520 deletions(-) diff --git a/web/app/components/workflow/persistence/local-storage-bridge.tsx b/web/app/components/workflow/persistence/local-storage-bridge.tsx index 31031e5348b..169d2ddcc41 100644 --- a/web/app/components/workflow/persistence/local-storage-bridge.tsx +++ b/web/app/components/workflow/persistence/local-storage-bridge.tsx @@ -1,4 +1,4 @@ -import { useEffect, useLayoutEffect as useLayoutEffectFromReact } from 'react' +import { useEffect, useLayoutEffect } from 'react' import { useStore, useWorkflowStore } from '../store' import { isControlMode, @@ -9,10 +9,6 @@ import { useWorkflowVariableInspectPanelHeightValue, } from './local-storage-options' -const useIsoLayoutEffect = typeof document !== 'undefined' - ? useLayoutEffectFromReact - : useEffect - export const WorkflowLocalStorageBridge = () => { const storedNodePanelWidth = useWorkflowNodePanelWidthValue() const storedPreviewPanelWidth = useDebugPreviewPanelWidthValue() @@ -26,7 +22,7 @@ export const WorkflowLocalStorageBridge = () => { const setVariableInspectPanelHeight = useStore(state => state.setVariableInspectPanelHeight) const setControlMode = useStore(state => state.setControlMode) - useIsoLayoutEffect(() => { + useLayoutEffect(() => { if (!isFiniteNumber(storedNodePanelWidth)) return @@ -34,17 +30,17 @@ export const WorkflowLocalStorageBridge = () => { setPanelWidth(storedNodePanelWidth) }, [setNodePanelWidth, setPanelWidth, storedNodePanelWidth]) - useIsoLayoutEffect(() => { + useLayoutEffect(() => { if (isFiniteNumber(storedPreviewPanelWidth)) setPreviewPanelWidth(storedPreviewPanelWidth) }, [setPreviewPanelWidth, storedPreviewPanelWidth]) - useIsoLayoutEffect(() => { + useLayoutEffect(() => { if (isFiniteNumber(storedVariableInspectPanelHeight)) setVariableInspectPanelHeight(storedVariableInspectPanelHeight) }, [setVariableInspectPanelHeight, storedVariableInspectPanelHeight]) - useIsoLayoutEffect(() => { + useLayoutEffect(() => { if (isControlMode(storedControlMode)) setControlMode(storedControlMode) }, [setControlMode, storedControlMode]) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx index 25fba5b077f..7a65452fb56 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' +import { fireEvent, render, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useAtomValue } from 'jotai' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -116,6 +116,12 @@ function renderKnowledgeRetrieval({ ) } +function getDialogNameEditButton(dialog: HTMLElement) { + return within(dialog).getByRole('button', { + name: /agentDetail\.configure\.knowledgeRetrieval\.edit/, + }) +} + describe('AgentKnowledgeRetrieval', () => { beforeEach(() => { vi.clearAllMocks() @@ -141,30 +147,6 @@ describe('AgentKnowledgeRetrieval', () => { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.remove:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}', })).not.toBeInTheDocument() }) - - it('should keep row actions out of layout until hover or focus', () => { - renderKnowledgeRetrieval() - - const editButton = screen.getByRole('button', { - name: 'agentV2.agentDetail.configure.knowledgeRetrieval.edit:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}', - }) - const removeButton = screen.getByRole('button', { - name: 'agentV2.agentDetail.configure.knowledgeRetrieval.remove:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}', - }) - const actionGroup = editButton.parentElement - - expect(actionGroup).toHaveClass('hidden') - expect(actionGroup).toHaveClass( - 'group-focus-within:flex', - 'group-hover:flex', - ) - expect(removeButton).toHaveClass( - 'hover:bg-state-destructive-hover', - 'hover:text-text-destructive', - 'focus-visible:bg-state-destructive-hover', - 'focus-visible:text-text-destructive', - ) - }) }) describe('User Interactions', () => { @@ -177,10 +159,9 @@ describe('AgentKnowledgeRetrieval', () => { const dialog = screen.getByRole('dialog', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', }) - const titleButton = within(dialog).getByRole('button', { - name: 'agentV2.agentDetail.configure.knowledgeRetrieval.retrievalTwo', - }) + const titleButton = getDialogNameEditButton(dialog) expect(titleButton).toBeInTheDocument() + expect(titleButton).toHaveTextContent('agentV2.agentDetail.configure.knowledgeRetrieval.retrievalTwo') expect(within(dialog).queryByRole('textbox', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.nameLabel', })).not.toBeInTheDocument() @@ -232,10 +213,6 @@ describe('AgentKnowledgeRetrieval', () => { await user.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.add' })) - expect(screen.queryByRole('button', { - name: 'agentV2.agentDetail.configure.knowledgeRetrieval.edit:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalTwo"}', - })).not.toBeInTheDocument() - await user.click(screen.getByRole('button', { name: 'Close' })) expect(screen.queryByRole('button', { @@ -318,9 +295,7 @@ describe('AgentKnowledgeRetrieval', () => { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', }) - await user.click(within(dialog).getByRole('button', { - name: 'FAQ Search', - })) + await user.click(getDialogNameEditButton(dialog)) const nameInput = within(dialog).getByRole('textbox', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.nameLabel', }) @@ -340,9 +315,7 @@ describe('AgentKnowledgeRetrieval', () => { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', }) - expect(within(dialog).getByRole('button', { - name: 'agentV2.agentDetail.configure.knowledgeRetrieval.retrievalTwo', - })).toBeInTheDocument() + expect(getDialogNameEditButton(dialog)).toHaveTextContent('agentV2.agentDetail.configure.knowledgeRetrieval.retrievalTwo') await user.click(within(dialog).getByRole('radio', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.query.custom', @@ -401,9 +374,7 @@ describe('AgentKnowledgeRetrieval', () => { const dialog = screen.getByRole('dialog', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', }) - await user.click(within(dialog).getByRole('button', { - name: 'agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne', - })) + await user.click(getDialogNameEditButton(dialog)) expect(within(dialog).getByRole('textbox', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.nameLabel', @@ -443,7 +414,7 @@ describe('AgentKnowledgeRetrieval', () => { expect(within(dialog).queryByText('appDebug.datasetConfig.knowledgeTip')).not.toBeInTheDocument() }) - it('should save the default rerank model when editing retrieval with existing knowledge', async () => { + it('should preserve retrieval settings when renaming a retrieval', async () => { const user = userEvent.setup() renderKnowledgeRetrieval({ showConfigSnapshot: true, @@ -477,27 +448,73 @@ describe('AgentKnowledgeRetrieval', () => { const dialog = screen.getByRole('dialog', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', }) - await user.click(within(dialog).getByRole('button', { - name: 'Search Docs', - })) + await user.click(getDialogNameEditButton(dialog)) const nameInput = within(dialog).getByRole('textbox', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.nameLabel', }) await user.clear(nameInput) - await user.type(nameInput, 'Default Rerank Docs') + await user.type(nameInput, 'Renamed Docs{Enter}') - await waitFor(() => { - const knowledgeConfig = JSON.parse(screen.getByLabelText('config snapshot').textContent ?? '{}') - expect(knowledgeConfig.sets[0].retrieval).toEqual(expect.objectContaining({ + const titleButton = getDialogNameEditButton(dialog) + expect(titleButton).toHaveTextContent('Renamed Docs') + expect(titleButton).toHaveFocus() + const knowledgeConfig = JSON.parse(screen.getByLabelText('config snapshot').textContent ?? '{}') + expect(knowledgeConfig.sets[0]).toMatchObject({ + name: 'Renamed Docs', + retrieval: { mode: 'multiple', - reranking_enable: true, - reranking_mode: RerankingModeEnum.RerankingModel, - reranking_model: { - provider: 'rerank-provider', - model: 'rerank-model', - }, - })) + reranking_enable: false, + top_k: 4, + }, }) + expect(knowledgeConfig.sets[0].retrieval).not.toHaveProperty('reranking_model') + }) + + it('should cancel name editing with Escape without closing the dialog', async () => { + const user = userEvent.setup() + renderKnowledgeRetrieval({ showConfigSnapshot: true }) + + await user.click(screen.getByRole('button', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.edit:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}', + })) + const dialog = screen.getByRole('dialog', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', + }) + await user.click(getDialogNameEditButton(dialog)) + const nameInput = within(dialog).getByRole('textbox', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.nameLabel', + }) + await user.clear(nameInput) + await user.type(nameInput, 'Draft name') + await user.keyboard('{Escape}') + + expect(dialog).toBeInTheDocument() + const titleButton = getDialogNameEditButton(dialog) + expect(titleButton).toHaveTextContent('agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne') + expect(titleButton).toHaveFocus() + const knowledgeConfig = JSON.parse(screen.getByLabelText('config snapshot').textContent ?? '{}') + expect(knowledgeConfig.sets[0].name).toBe('agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne') + }) + + it('should keep editing when Enter confirms an IME composition', async () => { + const user = userEvent.setup() + renderKnowledgeRetrieval() + + await user.click(screen.getByRole('button', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.edit:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}', + })) + const dialog = screen.getByRole('dialog', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', + }) + await user.click(getDialogNameEditButton(dialog)) + const nameInput = within(dialog).getByRole('textbox', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.nameLabel', + }) + + fireEvent.keyDown(nameInput, { key: 'Enter', isComposing: true }) + + expect(nameInput).toBeInTheDocument() + expect(nameInput).toHaveFocus() }) it('should save edited retrieval data into the config snapshot', async () => { @@ -511,9 +528,7 @@ describe('AgentKnowledgeRetrieval', () => { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', }) - await user.click(within(dialog).getByRole('button', { - name: 'agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne', - })) + await user.click(getDialogNameEditButton(dialog)) const nameInput = within(dialog).getByRole('textbox', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.nameLabel', }) @@ -571,5 +586,33 @@ describe('AgentKnowledgeRetrieval', () => { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', })).not.toBeInTheDocument() }) + + it('should reset an uncreated dialog draft after closing', async () => { + const user = userEvent.setup() + renderKnowledgeRetrieval() + + await user.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.add' })) + let dialog = screen.getByRole('dialog', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', + }) + await user.click(within(dialog).getByRole('radio', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.query.custom', + })) + await user.type(within(dialog).getByRole('textbox', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.query.customInputLabel', + }), 'temporary query') + await user.click(within(dialog).getByRole('button', { name: 'Close' })) + + await user.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.knowledgeRetrieval.add' })) + dialog = screen.getByRole('dialog', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.title', + }) + expect(within(dialog).getByRole('radio', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.query.agent', + })).toBeChecked() + expect(within(dialog).queryByRole('textbox', { + name: 'agentV2.agentDetail.configure.knowledgeRetrieval.dialog.query.customInputLabel', + })).not.toBeInTheDocument() + }) }) }) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx index 65370769ac6..c67838e3667 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/knowledge/dialog.tsx @@ -1,7 +1,7 @@ 'use client' import type { AgentKnowledgeDatasetConfig } from '@dify/contracts/api/console/agent/types.gen' -import type { ReactNode, SetStateAction } from 'react' +import type { ReactNode } from 'react' import type { MetadataFilteringCondition, MetadataFilteringModeEnum, @@ -18,7 +18,7 @@ import { RadioGroup, RadioItem } from '@langgenius/dify-ui/radio' import { Textarea } from '@langgenius/dify-ui/textarea' import { intersectionBy } from 'es-toolkit/compat' import { useAtomValue } from 'jotai' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useLayoutEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { IndexingType } from '@/app/components/datasets/create/step-two/hooks/use-indexing-config' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -50,20 +50,26 @@ type MetadataFilteringConditions = { type KnowledgeRetrievalDialogState = { customQuery: string - hydratedKey: string | null - isEditingName: boolean metadataFilterMode: MetadataFilteringModeEnum metadataFilteringConditions: MetadataFilteringConditions metadataModelConfig?: ModelConfig multipleRetrievalConfig: MultipleRetrievalConfig name: string queryMode: KnowledgeRetrievalQueryMode - rerankModelOpen: boolean retrievalMode: typeof RETRIEVE_TYPE[keyof typeof RETRIEVE_TYPE] selectedDatasets: DataSet[] singleRetrievalConfig?: SingleRetrievalConfig } +type AgentKnowledgeRetrievalDialogProps = { + item?: AgentKnowledgeRetrievalItem + initialName?: string + onItemCreate?: (item: AgentKnowledgeRetrievalItem) => void + onItemChange?: (item: AgentKnowledgeRetrievalItem) => void + open: boolean + onOpenChange: (open: boolean) => void +} + const queryModeOptions: KnowledgeRetrievalQueryMode[] = ['agent', 'custom'] const optionCardClassName = cn( @@ -191,18 +197,14 @@ const createDialogState = ( item: AgentKnowledgeRetrievalItem | undefined, initialName: string | undefined, fallbackName: string, - hydrationKey: string, ): KnowledgeRetrievalDialogState => ({ customQuery: item?.customQuery ?? '', - hydratedKey: hydrationKey, - isEditingName: false, metadataFilterMode: item?.metadataFilterMode ?? WorkflowMetadataFilteringModeEnum.disabled, metadataFilteringConditions: item?.metadataFilteringConditions ?? createDefaultMetadataFilteringConditions(), metadataModelConfig: item?.metadataModelConfig, multipleRetrievalConfig: item?.multipleRetrievalConfig ?? createDefaultRetrievalConfig(), name: getDialogName(item, initialName, fallbackName), queryMode: item?.queryMode ?? 'agent', - rerankModelOpen: false, retrievalMode: item?.retrievalMode ?? RETRIEVE_TYPE.multiWay, selectedDatasets: getSelectedDatasets(item), singleRetrievalConfig: item?.singleRetrievalConfig, @@ -217,41 +219,130 @@ const createMetadataCondition = ({ id, name, type }: MetadataInDoc): MetadataFil : ComparisonOperator.is, }) -export function AgentKnowledgeRetrievalDialog({ +function EditableKnowledgeRetrievalName({ + editLabel, + inputLabel, + invalid, + name, + onCommit, +}: { + editLabel: string + inputLabel: string + invalid: boolean + name: string + onCommit: (name: string) => void +}) { + const [editing, setEditing] = useState(false) + const [draftName, setDraftName] = useState(name) + const buttonRef = useRef(null) + const inputRef = useRef(null) + const restoreButtonFocusRef = useRef(false) + + useLayoutEffect(() => { + if (editing) { + inputRef.current?.focus() + inputRef.current?.select() + return + } + + if (!restoreButtonFocusRef.current) + return + + restoreButtonFocusRef.current = false + const button = buttonRef.current + if (!button) + return + + const activeElement = button.ownerDocument.activeElement + if (!activeElement || activeElement === button.ownerDocument.body) + button.focus({ preventScroll: true }) + }, [editing]) + + const finishEditing = (commit: boolean) => { + if (commit) + onCommit(draftName) + else + setDraftName(name) + + restoreButtonFocusRef.current = true + setEditing(false) + } + + if (editing) { + return ( + { + onCommit(draftName) + setEditing(false) + }} + onChange={event => setDraftName(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.nativeEvent.isComposing) + return + + if (event.key === 'Enter') { + event.preventDefault() + finishEditing(true) + return + } + + if (event.key === 'Escape') { + event.preventDefault() + event.stopPropagation() + finishEditing(false) + } + }} + /> + ) + } + + return ( + + ) +} + +function AgentKnowledgeRetrievalDialogContent({ item, initialName, onItemCreate, onItemChange, - open, - onOpenChange, -}: { - item?: AgentKnowledgeRetrievalItem - initialName?: string - onItemCreate?: (item: AgentKnowledgeRetrievalItem) => void - onItemChange?: (item: AgentKnowledgeRetrievalItem) => void - open: boolean - onOpenChange: (open: boolean) => void -}) { +}: Omit) { const { t } = useTranslation('agentV2') const docLink = useDocLink() const retrievals = useAtomValue(agentComposerKnowledgeRetrievalsAtom) const getValidationMessage = useKnowledgeValidationMessage() const fallbackName = t($ => $['agentDetail.configure.knowledgeRetrieval.retrievalOne']) - const hydrationKey = open ? (item?.id ?? initialName ?? 'new') : null - const [dialogState, setDialogState] = useState(() => createDialogState(item, initialName, fallbackName, hydrationKey ?? 'new')) - const nameInputRef = useRef(null) + const [dialogState, setDialogState] = useState(() => createDialogState(item, initialName, fallbackName)) + const [rerankModelOpen, setRerankModelOpen] = useState(false) const queryModeLabelId = 'agent-knowledge-retrieval-query-mode-label' const { customQuery, - hydratedKey, - isEditingName, metadataFilterMode, metadataFilteringConditions, metadataModelConfig, multipleRetrievalConfig, name, queryMode, - rerankModelOpen, retrievalMode, selectedDatasets, singleRetrievalConfig, @@ -272,11 +363,11 @@ export function AgentKnowledgeRetrievalDialog({ } : undefined, ) - const fallbackRerankModel = useMemo(() => ({ + const fallbackRerankModel = { provider: currentRerankProvider?.provider, model: currentRerankModel?.model, - }), [currentRerankModel?.model, currentRerankProvider?.provider]) - const resolveMultipleRetrievalConfig = useCallback(( + } + const resolveMultipleRetrievalConfig = ( config: MultipleRetrievalConfig, nextSelectedDatasets = selectedDatasets, originalDatasets = selectedDatasets, @@ -285,112 +376,56 @@ export function AgentKnowledgeRetrievalDialog({ nextSelectedDatasets, originalDatasets, fallbackRerankModel, - ), [fallbackRerankModel, selectedDatasets]) - const effectiveMultipleRetrievalConfig = useMemo(() => { - if (retrievalMode !== RETRIEVE_TYPE.multiWay || selectedDatasets.length === 0) - return multipleRetrievalConfig - - return resolveMultipleRetrievalConfig(multipleRetrievalConfig) - }, [multipleRetrievalConfig, resolveMultipleRetrievalConfig, retrievalMode, selectedDatasets.length]) - const patchDialogState = (patch: Partial) => { - setDialogState(current => ({ - ...current, - ...patch, - })) - } - const setMetadataFilteringConditions = (update: SetStateAction) => { - setDialogState((current) => { - const nextMetadataFilteringConditions = typeof update === 'function' - ? update(current.metadataFilteringConditions) - : update - - return { - ...current, - metadataFilteringConditions: nextMetadataFilteringConditions, - } - }) - } - const setMetadataModelConfig = (update: SetStateAction) => { - setDialogState((current) => { - const nextMetadataModelConfig = typeof update === 'function' - ? update(current.metadataModelConfig) - : update - - return { - ...current, - metadataModelConfig: nextMetadataModelConfig, - } - }) - } - const setSingleRetrievalConfig = (update: SetStateAction) => { - setDialogState((current) => { - const nextSingleRetrievalConfig = typeof update === 'function' - ? update(current.singleRetrievalConfig) - : update - - return { - ...current, - singleRetrievalConfig: nextSingleRetrievalConfig, - } - }) - } - const createItemFromDialogState = (patch: Partial): AgentKnowledgeRetrievalItem => ({ - id: globalThis.crypto?.randomUUID?.() ?? `retrieval-${Date.now()}`, - name, - queryMode, - customQuery, - selectedDatasets, - retrievalMode, - multipleRetrievalConfig: effectiveMultipleRetrievalConfig, - singleRetrievalConfig, - metadataFilterMode, - metadataFilteringConditions, - metadataModelConfig, - ...patch, + ) + const effectiveMultipleRetrievalConfig = retrievalMode === RETRIEVE_TYPE.multiWay && selectedDatasets.length > 0 + ? resolveMultipleRetrievalConfig(multipleRetrievalConfig) + : multipleRetrievalConfig + const createItemFromDialogState = ( + state: KnowledgeRetrievalDialogState, + id = globalThis.crypto?.randomUUID?.() ?? `retrieval-${Date.now()}`, + ): AgentKnowledgeRetrievalItem => ({ + id, + name: state.name, + queryMode: state.queryMode, + customQuery: state.customQuery, + selectedDatasets: state.selectedDatasets, + retrievalMode: state.retrievalMode, + multipleRetrievalConfig: state.multipleRetrievalConfig, + singleRetrievalConfig: state.singleRetrievalConfig, + metadataFilterMode: state.metadataFilterMode, + metadataFilteringConditions: state.metadataFilteringConditions, + metadataModelConfig: state.metadataModelConfig, }) - const updateItem = (patch: Partial) => { - if (!item) - return - - onItemChange?.({ - ...item, - name, - queryMode, - customQuery, - selectedDatasets, - retrievalMode, - multipleRetrievalConfig: effectiveMultipleRetrievalConfig, - singleRetrievalConfig, - metadataFilterMode, - metadataFilteringConditions, - metadataModelConfig, + const applyDialogStatePatch = (patch: Partial) => { + const nextState = { + ...dialogState, ...patch, - }) + } + setDialogState(nextState) + + if (item) { + onItemChange?.({ + ...item, + ...createItemFromDialogState(nextState, item.id), + }) + } + + return nextState } const handleSelectedDatasetsChange = (nextDatasets: DataSet[]) => { const nextMultipleRetrievalConfig = retrievalMode === RETRIEVE_TYPE.multiWay && nextDatasets.length > 0 ? resolveMultipleRetrievalConfig(multipleRetrievalConfig, nextDatasets) : multipleRetrievalConfig - - patchDialogState({ + const nextState = applyDialogStatePatch({ multipleRetrievalConfig: nextMultipleRetrievalConfig, selectedDatasets: nextDatasets, }) - if (item) { - updateItem({ - multipleRetrievalConfig: nextMultipleRetrievalConfig, - selectedDatasets: nextDatasets, - }) + if (item) return - } - if (nextDatasets.length > 0) { - onItemCreate?.(createItemFromDialogState({ - multipleRetrievalConfig: nextMultipleRetrievalConfig, - selectedDatasets: nextDatasets, - })) - } + if (nextDatasets.length > 0) + onItemCreate?.(createItemFromDialogState(nextState)) } const metadataList = useMemo(() => { const datasetsWithMetadata = selectedDatasets.filter(dataset => !!dataset.doc_metadata) @@ -410,336 +445,284 @@ export function AgentKnowledgeRetrievalDialog({ ? undefined : getValidationMessage(itemValidation?.metadata) - useEffect(() => { - if (hydratedKey !== hydrationKey) { - // eslint-disable-next-line react/set-state-in-effect - setDialogState(current => hydrationKey - ? createDialogState(item, initialName, fallbackName, hydrationKey) - : { - ...current, - hydratedKey: null, - isEditingName: false, - }) - } - }, [fallbackName, hydratedKey, hydrationKey, initialName, item]) + return ( + <> + + {t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.title'])} + +
+ + $['agentDetail.configure.knowledgeRetrieval.edit'], { name })} + inputLabel={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.nameLabel'])} + invalid={!!nameError} + name={name} + onCommit={nextName => applyDialogStatePatch({ name: nextName })} + /> + +
+ {nameError && ( +
+ {nameError} +
+ )} - useEffect(() => { - if (!isEditingName) - return +
+
+ + {t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.label'])} + + + aria-labelledby={queryModeLabelId} + className="w-full gap-2" + value={queryMode} + onValueChange={(nextMode) => { + if (nextMode) + applyDialogStatePatch({ queryMode: nextMode }) + }} + > + {queryModeOptions.map(mode => ( + + key={mode} + value={mode} + nativeButton + render={