mirror of
https://github.com/langgenius/dify.git
synced 2026-07-11 23:48:08 +08:00
Compare commits
2 Commits
main
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
| 0220441452 | |||
| 515ecc9bb3 |
@ -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])
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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<HTMLButtonElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(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 (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
aria-label={inputLabel}
|
||||
aria-invalid={invalid || undefined}
|
||||
autoComplete="off"
|
||||
className="h-7 min-w-0 flex-1 rounded-md px-1 py-0 system-xl-semibold text-text-primary"
|
||||
name="knowledgeRetrievalName"
|
||||
value={draftName}
|
||||
onBlur={() => {
|
||||
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 (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
aria-label={editLabel}
|
||||
className="flex h-7 min-w-0 flex-1 items-center rounded-md border border-transparent px-1 py-0 text-left system-xl-semibold text-text-primary hover:bg-components-input-bg-hover focus-visible:border-components-input-border-active focus-visible:bg-components-input-bg-active focus-visible:shadow-xs focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
onClick={() => {
|
||||
setDraftName(name)
|
||||
setEditing(true)
|
||||
}}
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
{name}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
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<AgentKnowledgeRetrievalDialogProps, 'open' | 'onOpenChange'>) {
|
||||
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<HTMLInputElement>(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<KnowledgeRetrievalDialogState>) => {
|
||||
setDialogState(current => ({
|
||||
...current,
|
||||
...patch,
|
||||
}))
|
||||
}
|
||||
const setMetadataFilteringConditions = (update: SetStateAction<MetadataFilteringConditions>) => {
|
||||
setDialogState((current) => {
|
||||
const nextMetadataFilteringConditions = typeof update === 'function'
|
||||
? update(current.metadataFilteringConditions)
|
||||
: update
|
||||
|
||||
return {
|
||||
...current,
|
||||
metadataFilteringConditions: nextMetadataFilteringConditions,
|
||||
}
|
||||
})
|
||||
}
|
||||
const setMetadataModelConfig = (update: SetStateAction<ModelConfig | undefined>) => {
|
||||
setDialogState((current) => {
|
||||
const nextMetadataModelConfig = typeof update === 'function'
|
||||
? update(current.metadataModelConfig)
|
||||
: update
|
||||
|
||||
return {
|
||||
...current,
|
||||
metadataModelConfig: nextMetadataModelConfig,
|
||||
}
|
||||
})
|
||||
}
|
||||
const setSingleRetrievalConfig = (update: SetStateAction<SingleRetrievalConfig | undefined>) => {
|
||||
setDialogState((current) => {
|
||||
const nextSingleRetrievalConfig = typeof update === 'function'
|
||||
? update(current.singleRetrievalConfig)
|
||||
: update
|
||||
|
||||
return {
|
||||
...current,
|
||||
singleRetrievalConfig: nextSingleRetrievalConfig,
|
||||
}
|
||||
})
|
||||
}
|
||||
const createItemFromDialogState = (patch: Partial<AgentKnowledgeRetrievalItem>): 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<AgentKnowledgeRetrievalItem>) => {
|
||||
if (!item)
|
||||
return
|
||||
|
||||
onItemChange?.({
|
||||
...item,
|
||||
name,
|
||||
queryMode,
|
||||
customQuery,
|
||||
selectedDatasets,
|
||||
retrievalMode,
|
||||
multipleRetrievalConfig: effectiveMultipleRetrievalConfig,
|
||||
singleRetrievalConfig,
|
||||
metadataFilterMode,
|
||||
metadataFilteringConditions,
|
||||
metadataModelConfig,
|
||||
const applyDialogStatePatch = (patch: Partial<KnowledgeRetrievalDialogState>) => {
|
||||
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 (
|
||||
<>
|
||||
<DialogTitle className="sr-only">
|
||||
{t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.title'])}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-2 px-4 pt-3">
|
||||
<KnowledgeRetrievalDialogIcon />
|
||||
<EditableKnowledgeRetrievalName
|
||||
editLabel={t($ => $['agentDetail.configure.knowledgeRetrieval.edit'], { name })}
|
||||
inputLabel={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.nameLabel'])}
|
||||
invalid={!!nameError}
|
||||
name={name}
|
||||
onCommit={nextName => applyDialogStatePatch({ name: nextName })}
|
||||
/>
|
||||
<DialogCloseButton className="static size-7 shrink-0 rounded-md" />
|
||||
</div>
|
||||
{nameError && (
|
||||
<div role="alert" className="px-4 pt-1 system-xs-regular text-text-destructive">
|
||||
{nameError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditingName)
|
||||
return
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-1 py-2">
|
||||
<div className="flex flex-col gap-1 px-4 py-2">
|
||||
<DialogFormLabel id={queryModeLabelId}>
|
||||
{t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.label'])}
|
||||
</DialogFormLabel>
|
||||
<RadioGroup<KnowledgeRetrievalQueryMode>
|
||||
aria-labelledby={queryModeLabelId}
|
||||
className="w-full gap-2"
|
||||
value={queryMode}
|
||||
onValueChange={(nextMode) => {
|
||||
if (nextMode)
|
||||
applyDialogStatePatch({ queryMode: nextMode })
|
||||
}}
|
||||
>
|
||||
{queryModeOptions.map(mode => (
|
||||
<RadioItem<KnowledgeRetrievalQueryMode>
|
||||
key={mode}
|
||||
value={mode}
|
||||
nativeButton
|
||||
render={<button type="button" className={optionCardClassName} />}
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
{t($ => $[`agentDetail.configure.knowledgeRetrieval.dialog.query.${mode}`])}
|
||||
</span>
|
||||
</RadioItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{queryMode === 'custom'
|
||||
? (
|
||||
<>
|
||||
<div className="pt-1">
|
||||
<Textarea
|
||||
aria-label={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.customInputLabel'])}
|
||||
aria-invalid={queryError ? true : undefined}
|
||||
className="h-20 resize-none rounded-lg px-3 py-2 system-sm-regular"
|
||||
placeholder={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.customPlaceholder'])}
|
||||
value={customQuery}
|
||||
onValueChange={nextQuery => applyDialogStatePatch({ customQuery: nextQuery })}
|
||||
/>
|
||||
</div>
|
||||
<p className="system-xs-regular text-text-tertiary">
|
||||
{t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.customDescription'])}
|
||||
</p>
|
||||
{queryError && (
|
||||
<p role="alert" className="system-xs-regular text-text-destructive">
|
||||
{queryError}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<p className="pt-1 system-xs-regular text-text-tertiary">
|
||||
{t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.agentDescription'])}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
nameInputRef.current?.focus()
|
||||
nameInputRef.current?.select()
|
||||
}, [isEditingName])
|
||||
<div className="px-4 py-2">
|
||||
<Field
|
||||
title={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.knowledge.label'])}
|
||||
required
|
||||
operations={(
|
||||
<div className="flex items-center space-x-1">
|
||||
<RetrievalConfig
|
||||
payload={{
|
||||
retrieval_mode: retrievalMode,
|
||||
multiple_retrieval_config: effectiveMultipleRetrievalConfig,
|
||||
single_retrieval_config: singleRetrievalConfig,
|
||||
}}
|
||||
onRetrievalModeChange={(nextRetrievalMode) => {
|
||||
const nextMultipleRetrievalConfig = nextRetrievalMode === RETRIEVE_TYPE.multiWay
|
||||
? resolveMultipleRetrievalConfig(multipleRetrievalConfig)
|
||||
: multipleRetrievalConfig
|
||||
|
||||
applyDialogStatePatch({
|
||||
multipleRetrievalConfig: nextMultipleRetrievalConfig,
|
||||
retrievalMode: nextRetrievalMode,
|
||||
})
|
||||
}}
|
||||
onMultipleRetrievalConfigChange={(nextMultipleRetrievalConfig) => {
|
||||
const normalizedMultipleRetrievalConfig = resolveMultipleRetrievalConfig(nextMultipleRetrievalConfig)
|
||||
|
||||
applyDialogStatePatch({ multipleRetrievalConfig: normalizedMultipleRetrievalConfig })
|
||||
}}
|
||||
singleRetrievalModelConfig={singleRetrievalConfig?.model}
|
||||
onSingleRetrievalModelChange={(model) => {
|
||||
applyDialogStatePatch({
|
||||
singleRetrievalConfig: {
|
||||
model: {
|
||||
provider: model.provider,
|
||||
name: model.modelId,
|
||||
mode: model.mode ?? singleRetrievalConfig?.model.mode ?? AppModeEnum.CHAT,
|
||||
completion_params: singleRetrievalConfig?.model.completion_params ?? { temperature: 0.7 },
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
onSingleRetrievalModelParamsChange={(completionParams) => {
|
||||
applyDialogStatePatch({
|
||||
singleRetrievalConfig: {
|
||||
model: {
|
||||
provider: singleRetrievalConfig?.model.provider ?? '',
|
||||
name: singleRetrievalConfig?.model.name ?? '',
|
||||
mode: singleRetrievalConfig?.model.mode ?? AppModeEnum.CHAT,
|
||||
completion_params: completionParams,
|
||||
},
|
||||
},
|
||||
})
|
||||
}}
|
||||
readonly={!selectedDatasets.length}
|
||||
modal
|
||||
rerankModalOpen={rerankModelOpen}
|
||||
onRerankModelOpenChange={setRerankModelOpen}
|
||||
selectedDatasets={selectedDatasets}
|
||||
/>
|
||||
<div className="h-3 w-px bg-divider-regular" />
|
||||
<AddKnowledge
|
||||
selectedIds={selectedDatasets.map(dataset => dataset.id)}
|
||||
modal
|
||||
onChange={handleSelectedDatasetsChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{selectedDatasets.length > 0 && (
|
||||
<DatasetList
|
||||
list={selectedDatasets}
|
||||
onChange={handleSelectedDatasetsChange}
|
||||
settingsDrawerBackdropClassName="bg-background-overlay"
|
||||
settingsDrawerBackdropForceRender
|
||||
settingsDrawerPopupClassName="data-[swipe-direction=right]:top-6 data-[swipe-direction=right]:bottom-6"
|
||||
settingsModalHeight="100%"
|
||||
/>
|
||||
)}
|
||||
{(datasetsError || retrievalError) && (
|
||||
<div role="alert" className="pt-2 system-xs-regular text-text-destructive">
|
||||
{datasetsError ?? retrievalError}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
<MetadataFilter
|
||||
metadataList={metadataList}
|
||||
selectedDatasetsLoaded
|
||||
metadataFilterMode={metadataFilterMode}
|
||||
metadataFilteringConditions={metadataFilteringConditions}
|
||||
handleMetadataFilterModeChange={nextMode => applyDialogStatePatch({ metadataFilterMode: nextMode })}
|
||||
handleAddCondition={(metadataItem) => {
|
||||
applyDialogStatePatch({
|
||||
metadataFilteringConditions: {
|
||||
...metadataFilteringConditions,
|
||||
conditions: [
|
||||
...metadataFilteringConditions.conditions,
|
||||
createMetadataCondition(metadataItem),
|
||||
],
|
||||
},
|
||||
})
|
||||
}}
|
||||
handleRemoveCondition={(conditionId) => {
|
||||
applyDialogStatePatch({
|
||||
metadataFilteringConditions: {
|
||||
...metadataFilteringConditions,
|
||||
conditions: metadataFilteringConditions.conditions.filter(condition => condition.id !== conditionId),
|
||||
},
|
||||
})
|
||||
}}
|
||||
handleToggleConditionLogicalOperator={() => {
|
||||
applyDialogStatePatch({
|
||||
metadataFilteringConditions: {
|
||||
...metadataFilteringConditions,
|
||||
logical_operator: metadataFilteringConditions.logical_operator === LogicalOperator.and
|
||||
? LogicalOperator.or
|
||||
: LogicalOperator.and,
|
||||
},
|
||||
})
|
||||
}}
|
||||
handleUpdateCondition={(conditionId, nextCondition) => {
|
||||
applyDialogStatePatch({
|
||||
metadataFilteringConditions: {
|
||||
...metadataFilteringConditions,
|
||||
conditions: metadataFilteringConditions.conditions.map(condition => condition.id === conditionId ? nextCondition : condition),
|
||||
},
|
||||
})
|
||||
}}
|
||||
metadataModelConfig={metadataModelConfig}
|
||||
handleMetadataModelChange={(model) => {
|
||||
applyDialogStatePatch({
|
||||
metadataModelConfig: {
|
||||
provider: model.provider,
|
||||
name: model.modelId,
|
||||
mode: model.mode ?? metadataModelConfig?.mode ?? AppModeEnum.CHAT,
|
||||
completion_params: metadataModelConfig?.completion_params ?? { temperature: 0.7 },
|
||||
},
|
||||
})
|
||||
}}
|
||||
handleMetadataCompletionParamsChange={(completionParams) => {
|
||||
applyDialogStatePatch({
|
||||
metadataModelConfig: {
|
||||
provider: metadataModelConfig?.provider ?? '',
|
||||
name: metadataModelConfig?.name ?? '',
|
||||
mode: metadataModelConfig?.mode ?? AppModeEnum.CHAT,
|
||||
completion_params: completionParams,
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{metadataError && (
|
||||
<div role="alert" className="px-4 pt-2 system-xs-regular text-text-destructive">
|
||||
{metadataError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 px-4 pb-4">
|
||||
<div aria-hidden className="h-2 w-8 border-b border-divider-regular" />
|
||||
<a
|
||||
href={docLink('/use-dify/knowledge/create-knowledge/setting-indexing-methods')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex min-w-0 items-center gap-1 system-xs-regular text-text-tertiary hover:text-text-secondary hover:underline focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
>
|
||||
<span aria-hidden className="i-ri-book-read-line size-3 shrink-0" />
|
||||
<span className="min-w-0 truncate">
|
||||
{t($ => $['form.retrievalSetting.learnMore'], { ns: 'datasetSettings' })}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentKnowledgeRetrievalDialog({
|
||||
item,
|
||||
initialName,
|
||||
onItemCreate,
|
||||
onItemChange,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AgentKnowledgeRetrievalDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
backdropProps={{ forceRender: true }}
|
||||
backdropClassName="fixed"
|
||||
className="flex h-[520px] max-h-[calc(100dvh-2rem)] w-[400px] flex-col overflow-hidden p-0"
|
||||
className="flex h-130 max-h-[calc(100dvh-2rem)] w-[400px] flex-col overflow-hidden p-0"
|
||||
>
|
||||
<DialogTitle className="sr-only">
|
||||
{t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.title'])}
|
||||
</DialogTitle>
|
||||
<div className="flex items-center gap-2 px-4 pt-3">
|
||||
<KnowledgeRetrievalDialogIcon />
|
||||
{isEditingName
|
||||
? (
|
||||
<Input
|
||||
ref={nameInputRef}
|
||||
aria-label={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.nameLabel'])}
|
||||
aria-invalid={nameError ? true : undefined}
|
||||
className="h-7 min-w-0 flex-1 rounded-md px-1 py-0 system-xl-semibold text-text-primary"
|
||||
value={name}
|
||||
onBlur={() => patchDialogState({ isEditingName: false })}
|
||||
onChange={(event) => {
|
||||
const nextName = event.currentTarget.value
|
||||
patchDialogState({ name: nextName })
|
||||
updateItem({ name: nextName })
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter')
|
||||
patchDialogState({ isEditingName: false })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-7 min-w-0 flex-1 items-center rounded-md px-1 py-0 text-left system-xl-semibold text-text-primary hover:bg-components-input-bg-hover focus-visible:border focus-visible:border-components-input-border-active focus-visible:bg-components-input-bg-active focus-visible:shadow-xs focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
onClick={() => patchDialogState({ isEditingName: true })}
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
{name}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<DialogCloseButton className="static size-7 shrink-0 rounded-md" />
|
||||
</div>
|
||||
{nameError && (
|
||||
<div role="alert" className="px-4 pt-1 system-xs-regular text-text-destructive">
|
||||
{nameError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-1 py-2">
|
||||
<div className="flex flex-col gap-1 px-4 py-2">
|
||||
<DialogFormLabel id={queryModeLabelId}>
|
||||
{t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.label'])}
|
||||
</DialogFormLabel>
|
||||
<RadioGroup<KnowledgeRetrievalQueryMode>
|
||||
aria-labelledby={queryModeLabelId}
|
||||
className="w-full gap-2"
|
||||
value={queryMode}
|
||||
onValueChange={(nextMode) => {
|
||||
if (nextMode) {
|
||||
patchDialogState({ queryMode: nextMode })
|
||||
updateItem({ queryMode: nextMode })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{queryModeOptions.map(mode => (
|
||||
<RadioItem<KnowledgeRetrievalQueryMode>
|
||||
key={mode}
|
||||
value={mode}
|
||||
nativeButton
|
||||
render={<button type="button" className={optionCardClassName} />}
|
||||
>
|
||||
<span className="min-w-0 truncate">
|
||||
{t($ => $[`agentDetail.configure.knowledgeRetrieval.dialog.query.${mode}`])}
|
||||
</span>
|
||||
</RadioItem>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{queryMode === 'custom'
|
||||
? (
|
||||
<>
|
||||
<div className="pt-1">
|
||||
<Textarea
|
||||
aria-label={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.customInputLabel'])}
|
||||
aria-invalid={queryError ? true : undefined}
|
||||
className="h-20 resize-none rounded-lg px-3 py-2 system-sm-regular"
|
||||
placeholder={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.customPlaceholder'])}
|
||||
value={customQuery}
|
||||
onValueChange={(nextQuery) => {
|
||||
patchDialogState({ customQuery: nextQuery })
|
||||
updateItem({ customQuery: nextQuery })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="system-xs-regular text-text-tertiary">
|
||||
{t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.customDescription'])}
|
||||
</p>
|
||||
{queryError && (
|
||||
<p role="alert" className="system-xs-regular text-text-destructive">
|
||||
{queryError}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<p className="pt-1 system-xs-regular text-text-tertiary">
|
||||
{t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.query.agentDescription'])}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-2">
|
||||
<Field
|
||||
title={t($ => $['agentDetail.configure.knowledgeRetrieval.dialog.knowledge.label'])}
|
||||
required
|
||||
operations={(
|
||||
<div className="flex items-center space-x-1">
|
||||
<RetrievalConfig
|
||||
payload={{
|
||||
retrieval_mode: retrievalMode,
|
||||
multiple_retrieval_config: effectiveMultipleRetrievalConfig,
|
||||
single_retrieval_config: singleRetrievalConfig,
|
||||
}}
|
||||
onRetrievalModeChange={(nextRetrievalMode) => {
|
||||
const nextMultipleRetrievalConfig = nextRetrievalMode === RETRIEVE_TYPE.multiWay
|
||||
? resolveMultipleRetrievalConfig(multipleRetrievalConfig)
|
||||
: multipleRetrievalConfig
|
||||
|
||||
patchDialogState({
|
||||
multipleRetrievalConfig: nextMultipleRetrievalConfig,
|
||||
retrievalMode: nextRetrievalMode,
|
||||
})
|
||||
updateItem({
|
||||
multipleRetrievalConfig: nextMultipleRetrievalConfig,
|
||||
retrievalMode: nextRetrievalMode,
|
||||
})
|
||||
}}
|
||||
onMultipleRetrievalConfigChange={(nextMultipleRetrievalConfig) => {
|
||||
const normalizedMultipleRetrievalConfig = resolveMultipleRetrievalConfig(nextMultipleRetrievalConfig)
|
||||
|
||||
patchDialogState({ multipleRetrievalConfig: normalizedMultipleRetrievalConfig })
|
||||
updateItem({ multipleRetrievalConfig: normalizedMultipleRetrievalConfig })
|
||||
}}
|
||||
singleRetrievalModelConfig={singleRetrievalConfig?.model}
|
||||
onSingleRetrievalModelChange={(model) => {
|
||||
setSingleRetrievalConfig((current) => {
|
||||
const nextSingleRetrievalConfig = {
|
||||
model: {
|
||||
provider: model.provider,
|
||||
name: model.modelId,
|
||||
mode: model.mode ?? current?.model.mode ?? AppModeEnum.CHAT,
|
||||
completion_params: current?.model.completion_params ?? { temperature: 0.7 },
|
||||
},
|
||||
}
|
||||
updateItem({ singleRetrievalConfig: nextSingleRetrievalConfig })
|
||||
return nextSingleRetrievalConfig
|
||||
})
|
||||
}}
|
||||
onSingleRetrievalModelParamsChange={(completionParams) => {
|
||||
setSingleRetrievalConfig((current) => {
|
||||
const nextSingleRetrievalConfig = {
|
||||
model: {
|
||||
provider: current?.model.provider ?? '',
|
||||
name: current?.model.name ?? '',
|
||||
mode: current?.model.mode ?? AppModeEnum.CHAT,
|
||||
completion_params: completionParams,
|
||||
},
|
||||
}
|
||||
updateItem({ singleRetrievalConfig: nextSingleRetrievalConfig })
|
||||
return nextSingleRetrievalConfig
|
||||
})
|
||||
}}
|
||||
readonly={!selectedDatasets.length}
|
||||
modal
|
||||
rerankModalOpen={rerankModelOpen}
|
||||
onRerankModelOpenChange={nextOpen => patchDialogState({ rerankModelOpen: nextOpen })}
|
||||
selectedDatasets={selectedDatasets}
|
||||
/>
|
||||
<div className="h-3 w-px bg-divider-regular" />
|
||||
<AddKnowledge
|
||||
selectedIds={selectedDatasets.map(dataset => dataset.id)}
|
||||
modal
|
||||
onChange={handleSelectedDatasetsChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{selectedDatasets.length > 0 && (
|
||||
<DatasetList
|
||||
list={selectedDatasets}
|
||||
onChange={handleSelectedDatasetsChange}
|
||||
settingsDrawerBackdropClassName="bg-background-overlay"
|
||||
settingsDrawerBackdropForceRender
|
||||
settingsDrawerPopupClassName="data-[swipe-direction=right]:top-6 data-[swipe-direction=right]:bottom-6"
|
||||
settingsModalHeight="100%"
|
||||
/>
|
||||
)}
|
||||
{(datasetsError || retrievalError) && (
|
||||
<div role="alert" className="pt-2 system-xs-regular text-text-destructive">
|
||||
{datasetsError ?? retrievalError}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="py-2">
|
||||
<MetadataFilter
|
||||
metadataList={metadataList}
|
||||
selectedDatasetsLoaded
|
||||
metadataFilterMode={metadataFilterMode}
|
||||
metadataFilteringConditions={metadataFilteringConditions}
|
||||
handleMetadataFilterModeChange={(nextMode) => {
|
||||
patchDialogState({ metadataFilterMode: nextMode })
|
||||
updateItem({ metadataFilterMode: nextMode })
|
||||
}}
|
||||
handleAddCondition={(metadataItem) => {
|
||||
setMetadataFilteringConditions((current) => {
|
||||
const nextConditions = {
|
||||
...current,
|
||||
conditions: [...current.conditions, createMetadataCondition(metadataItem)],
|
||||
}
|
||||
updateItem({ metadataFilteringConditions: nextConditions })
|
||||
return nextConditions
|
||||
})
|
||||
}}
|
||||
handleRemoveCondition={(conditionId) => {
|
||||
setMetadataFilteringConditions((current) => {
|
||||
const nextConditions = {
|
||||
...current,
|
||||
conditions: current.conditions.filter(condition => condition.id !== conditionId),
|
||||
}
|
||||
updateItem({ metadataFilteringConditions: nextConditions })
|
||||
return nextConditions
|
||||
})
|
||||
}}
|
||||
handleToggleConditionLogicalOperator={() => {
|
||||
setMetadataFilteringConditions((current) => {
|
||||
const nextConditions = {
|
||||
...current,
|
||||
logical_operator: current.logical_operator === LogicalOperator.and
|
||||
? LogicalOperator.or
|
||||
: LogicalOperator.and,
|
||||
}
|
||||
updateItem({ metadataFilteringConditions: nextConditions })
|
||||
return nextConditions
|
||||
})
|
||||
}}
|
||||
handleUpdateCondition={(conditionId, nextCondition) => {
|
||||
setMetadataFilteringConditions((current) => {
|
||||
const nextConditions = {
|
||||
...current,
|
||||
conditions: current.conditions.map(condition => condition.id === conditionId ? nextCondition : condition),
|
||||
}
|
||||
updateItem({ metadataFilteringConditions: nextConditions })
|
||||
return nextConditions
|
||||
})
|
||||
}}
|
||||
metadataModelConfig={metadataModelConfig}
|
||||
handleMetadataModelChange={(model) => {
|
||||
setMetadataModelConfig((current) => {
|
||||
const nextMetadataModelConfig = {
|
||||
provider: model.provider,
|
||||
name: model.modelId,
|
||||
mode: model.mode ?? current?.mode ?? AppModeEnum.CHAT,
|
||||
completion_params: current?.completion_params ?? { temperature: 0.7 },
|
||||
}
|
||||
updateItem({ metadataModelConfig: nextMetadataModelConfig })
|
||||
return nextMetadataModelConfig
|
||||
})
|
||||
}}
|
||||
handleMetadataCompletionParamsChange={(completionParams) => {
|
||||
setMetadataModelConfig((current) => {
|
||||
const nextMetadataModelConfig = {
|
||||
provider: current?.provider ?? '',
|
||||
name: current?.name ?? '',
|
||||
mode: current?.mode ?? AppModeEnum.CHAT,
|
||||
completion_params: completionParams,
|
||||
}
|
||||
updateItem({ metadataModelConfig: nextMetadataModelConfig })
|
||||
return nextMetadataModelConfig
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{metadataError && (
|
||||
<div role="alert" className="px-4 pt-2 system-xs-regular text-text-destructive">
|
||||
{metadataError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 px-4 pb-4">
|
||||
<div aria-hidden className="h-2 w-8 border-b border-divider-regular" />
|
||||
<a
|
||||
href={docLink('/use-dify/knowledge/create-knowledge/setting-indexing-methods')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex min-w-0 items-center gap-1 system-xs-regular text-text-tertiary hover:text-text-secondary hover:underline focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
>
|
||||
<span aria-hidden className="i-ri-book-read-line size-3 shrink-0" />
|
||||
<span className="min-w-0 truncate">
|
||||
{t($ => $['form.retrievalSetting.learnMore'], { ns: 'datasetSettings' })}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<AgentKnowledgeRetrievalDialogContent
|
||||
item={item}
|
||||
initialName={initialName}
|
||||
onItemCreate={onItemCreate}
|
||||
onItemChange={onItemChange}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
COMMAND_PRIORITY_LOW,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical'
|
||||
import { useCallback, useEffect, useMemo, useLayoutEffect as useReactLayoutEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
@ -44,11 +44,6 @@ import { useAgentPromptToolIconResolver } from './hooks'
|
||||
import { insertTokenAtTextRange, replaceTrailingSlashWithToken } from './options'
|
||||
import { AgentPromptSlashMenu } from './slash'
|
||||
|
||||
const noopLayoutEffect: typeof useReactLayoutEffect = () => {}
|
||||
const useIsoLayoutEffect = typeof document !== 'undefined'
|
||||
? useReactLayoutEffect
|
||||
: noopLayoutEffect
|
||||
|
||||
function AgentPromptPlaceholder({
|
||||
insertLabel,
|
||||
text,
|
||||
@ -870,7 +865,7 @@ export function AgentPromptEditor() {
|
||||
}
|
||||
}, [closeSlashMenu, isSlashMenuOpen])
|
||||
|
||||
useIsoLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!isSlashMenuOpen)
|
||||
return
|
||||
|
||||
@ -948,7 +943,7 @@ export function AgentPromptEditor() {
|
||||
}
|
||||
}, [activateSlashMenuItem, closeSlashMenu, focusPromptEditor, getSlashMenuItems, isSlashMenuOpen, returnToSlashMenuMain, setActiveSlashMenuItem, slashMenuView])
|
||||
|
||||
useIsoLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!isSlashMenuOpen)
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user