refactor(web): share new RAG setup controls

This commit is contained in:
Stephen Zhou 2026-08-10 22:51:39 +08:00
parent fbde89eeb9
commit 655e596106
No known key found for this signature in database
6 changed files with 199 additions and 199 deletions

View File

@ -44,8 +44,9 @@ import {
sourceProviderOptionForDraft,
} from './source-provider-options'
import {
SourceProviderIcon,
SourceProviderNotInstalledCard,
SourceProviderRadioGroup,
SourceProviderSelector,
SourceTypeSelector,
} from './source-setup-fields'
import { WebsiteCrawlPreview } from './website-crawl-preview'
@ -207,64 +208,6 @@ function getSupportedAuthKinds(provider: Provider, credentialId?: string) {
return supported
}
function ProviderSelector({
disabled = false,
onMoreProviders,
options,
providerKey,
onChange,
}: {
disabled?: boolean
onMoreProviders: () => void
options: SourceProviderOption[]
providerKey: string
onChange: (providerKey: string) => void
}) {
const { t } = useTranslation('dataset')
return (
<Fieldset disabled={disabled}>
<div className="mb-1.5 flex items-center justify-between gap-3">
<FieldsetLegend className="py-0 system-xs-medium">
{t(($) => $['newKnowledge.providerLabel'])}
</FieldsetLegend>
<Button
type="button"
variant="ghost-accent"
size="small"
className="gap-0.5 px-2.75"
disabled={disabled}
onClick={onMoreProviders}
>
{t(($) => $['newKnowledge.moreProviders'])}
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
</Button>
</div>
<SourceProviderRadioGroup
value={providerKey}
disabled={disabled}
layout="wrap"
options={options.map((option) => ({
icon: <WebsiteProviderIcon option={option} />,
label: option.label,
value: option.key,
}))}
size="small"
onChange={onChange}
/>
</Fieldset>
)
}
function WebsiteProviderIcon({ option }: { option: SourceProviderOption }) {
const icon = option.installed
? (option.datasource.identity.icon ?? option.plugin.declaration.identity.icon)
: undefined
if (typeof icon === 'string' && icon)
return <img aria-hidden alt="" className="size-4 shrink-0 object-contain" src={icon} />
return <span aria-hidden className={`${option.fallbackIcon} size-4 shrink-0`} />
}
function ProviderFieldControl({
field,
setValues,
@ -680,7 +623,15 @@ function UnconfiguredProvider({
return (
<div className="flex flex-col items-start gap-2.5 rounded-xl bg-background-section p-4">
<span className="flex size-9 items-center justify-center rounded-lg border border-divider-subtle bg-background-default">
<WebsiteProviderIcon option={providerOption} />
<SourceProviderIcon
fallbackIcon={providerOption.fallbackIcon}
icon={
providerOption.installed
? (providerOption.datasource.identity.icon ??
providerOption.plugin.declaration.identity.icon)
: undefined
}
/>
</span>
<h3 className="system-sm-semibold text-text-primary">
{t(($) => $['newKnowledge.providerNotConfigured'], {
@ -1311,7 +1262,7 @@ export function AddSourcePage({
/>
{sourceDraft.sourceType === 'websiteCrawl' ? (
<>
<ProviderSelector
<SourceProviderSelector
disabled={websiteSetupLocked}
options={websiteProviderOptions}
providerKey={websiteProviderOption?.key ?? ''}
@ -1351,7 +1302,7 @@ export function AddSourcePage({
</div>
) : websiteProviderOption && !websiteProviderOption.installed ? (
<SourceProviderNotInstalledCard
icon={<WebsiteProviderIcon option={websiteProviderOption} />}
icon={<SourceProviderIcon fallbackIcon={websiteProviderOption.fallbackIcon} />}
provider={websiteProviderOption.label}
onInstall={() =>
globalThis.open(

View File

@ -0,0 +1,56 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
import { useTranslation } from 'react-i18next'
const retrievalModes = ['fast', 'deep', 'research'] as const
export type RetrievalMode = (typeof retrievalModes)[number]
export function RetrievalModeSegmentedControl({
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
appearance = 'default',
disabled = false,
value,
onChange,
}: {
'aria-label'?: string
'aria-labelledby'?: string
appearance?: 'composer' | 'default'
disabled?: boolean
value: RetrievalMode
onChange: (value: RetrievalMode) => void
}) {
const { t } = useTranslation('dataset')
return (
<SegmentedControl<RetrievalMode>
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
className={cn(
appearance === 'composer' && 'flex min-w-46.5 gap-0.5 bg-background-section-burn',
)}
value={[value]}
onValueChange={(values) => {
const nextValue = values[0]
if (nextValue) onChange(nextValue)
}}
>
{retrievalModes.map((mode) => (
<SegmentedControlItem<RetrievalMode>
key={mode}
value={mode}
disabled={disabled}
className={cn(
appearance === 'composer' &&
'grow border-0 px-2.5 py-1.25 system-sm-regular text-text-tertiary capitalize data-pressed:bg-components-panel-bg data-pressed:font-medium data-pressed:text-text-primary',
)}
>
{t(($) => $[`newKnowledge.settings.retrievalMode.${mode}`])}
</SegmentedControlItem>
))}
</SegmentedControl>
)
}

View File

@ -15,11 +15,9 @@ import type {
DataSourceAuth,
DataSourceCredential,
} from '@/app/components/header/account-setting/data-source-page-new/types'
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset'
import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -46,8 +44,9 @@ import {
import {
SourceConnectionRequiredCard,
SourceNameField,
SourceProviderIcon,
SourceProviderNotInstalledCard,
SourceProviderRadioGroup,
SourceProviderSelector,
SourceSyncPolicyField,
} from './source-setup-fields'
@ -142,11 +141,9 @@ function datasourceProviderForOption(option?: SourceProviderOption) {
: undefined
}
type ProviderBrandIconValue = DataSourceItem['declaration']['identity']['icon']
function datasourceProviderIcon(
datasourceProvider: ReturnType<typeof datasourceProviderForOption>,
): ProviderBrandIconValue | undefined {
) {
return datasourceProvider?.plugin.declaration.identity.icon
}
@ -315,83 +312,6 @@ function requestStatus(error: unknown) {
return undefined
}
function ProviderSelector({
options,
providerKey,
onChange,
onMoreProviders,
}: {
options: SourceProviderOption[]
providerKey: string
onChange: (providerKey: string) => void
onMoreProviders: () => void
}) {
const { t } = useTranslation('dataset')
return (
<Fieldset>
<div className="mb-2 flex items-center justify-between gap-3">
<FieldsetLegend className="py-0 system-xs-medium">
{t(($) => $['newKnowledge.providerLabel'])}
</FieldsetLegend>
<Button
type="button"
variant="ghost-accent"
size="small"
className="h-6 gap-0.5 px-0"
onClick={onMoreProviders}
>
{t(($) => $['newKnowledge.moreProviders'])}
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
</Button>
</div>
<SourceProviderRadioGroup
value={providerKey}
layout="wrap"
options={options.map((option) => ({
icon: (
<ProviderBrandIcon
fallbackIcon={option.fallbackIcon}
icon={
option.installed
? (option.datasource.identity.icon ?? option.plugin.declaration.identity.icon)
: undefined
}
/>
),
label: option.label,
value: option.key,
}))}
size="small"
onChange={onChange}
/>
</Fieldset>
)
}
function ProviderBrandIcon({
fallbackIcon,
icon,
}: {
fallbackIcon: string
icon?: ProviderBrandIconValue
}) {
if (typeof icon === 'string' && icon)
return <img aria-hidden alt="" className="size-4 shrink-0 object-contain" src={icon} />
if (icon && typeof icon !== 'string')
return (
<span
aria-hidden
className="flex size-4 shrink-0 items-center justify-center overflow-hidden rounded text-2xs"
style={{ backgroundColor: icon.background }}
>
{icon.content}
</span>
)
return <span aria-hidden className={`${fallbackIcon} size-4 shrink-0`} />
}
function OAuthConnectionCard({
draft,
icon,
@ -399,7 +319,7 @@ function OAuthConnectionCard({
onConnect,
}: {
draft: ConnectedSourceDraft
icon?: ProviderBrandIconValue
icon?: ReturnType<typeof datasourceProviderIcon>
providerOption: InstalledSourceProviderOption
onConnect: () => void
}) {
@ -414,7 +334,7 @@ function OAuthConnectionCard({
provider: draft.provider,
})
}
icon={<ProviderBrandIcon fallbackIcon={providerOption.fallbackIcon} icon={icon} />}
icon={<SourceProviderIcon fallbackIcon={providerOption.fallbackIcon} icon={icon} />}
title={
draft.provider === 'Notion'
? t(($) => $['newKnowledge.notionNotConnected'])
@ -1824,7 +1744,8 @@ export function ConnectedSourceSetup({
}
return (
<div className="flex flex-col gap-4">
<ProviderSelector
<SourceProviderSelector
appearance="embedded"
options={providerOptions}
providerKey={providerOption?.key ?? ''}
onChange={selectProvider}
@ -1860,7 +1781,7 @@ export function ConnectedSourceSetup({
</div>
) : providerOption && !providerOption.installed ? (
<SourceProviderNotInstalledCard
icon={<ProviderBrandIcon fallbackIcon={providerOption.fallbackIcon} />}
icon={<SourceProviderIcon fallbackIcon={providerOption.fallbackIcon} />}
provider={providerOption.label}
onInstall={() =>
globalThis.open(

View File

@ -24,7 +24,6 @@ import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Form } from '@langgenius/dify-ui/form'
import { Input } from '@langgenius/dify-ui/input'
import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
import { Slider } from '@langgenius/dify-ui/slider'
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
@ -40,6 +39,7 @@ import { useRouter } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
import { KnowledgeSettingsMembers } from './components/knowledge-settings-members'
import { KnowledgeSpaceIcon } from './components/knowledge-space-icon'
import { RetrievalModeSegmentedControl } from './components/retrieval-mode-segmented-control'
import { isKnowledgeModelSetupReady, KNOWLEDGE_NAME_MAX_LENGTH } from './constants'
import { newKnowledgeListPath } from './routes'
@ -1024,30 +1024,22 @@ export function KnowledgeSettingsForm({
>
{t(($) => $['newKnowledge.settings.retrievalDepth'])}
</label>
<SegmentedControl
<RetrievalModeSegmentedControl
aria-labelledby="knowledge-retrieval-depth-label"
value={[retrievalMode]}
onValueChange={(values) => {
const mode = values[0]
if (mode === 'fast' || mode === 'deep' || mode === 'research') {
const nextScoreThresholdEnabled =
mode !== 'research' && !rerankEnabled ? false : scoreThresholdEnabled
setRetrievalMode(mode)
setScoreThresholdEnabled(nextScoreThresholdEnabled)
void performSettingsSave({
...currentSettingsDraft,
retrievalMode: mode,
scoreThresholdEnabled: nextScoreThresholdEnabled,
})
}
disabled={retrievalFieldsDisabled}
value={retrievalMode}
onChange={(mode) => {
const nextScoreThresholdEnabled =
mode !== 'research' && !rerankEnabled ? false : scoreThresholdEnabled
setRetrievalMode(mode)
setScoreThresholdEnabled(nextScoreThresholdEnabled)
void performSettingsSave({
...currentSettingsDraft,
retrievalMode: mode,
scoreThresholdEnabled: nextScoreThresholdEnabled,
})
}}
>
{(['fast', 'deep', 'research'] as const).map((mode) => (
<SegmentedControlItem key={mode} value={mode} disabled={retrievalFieldsDisabled}>
{t(($) => $[`newKnowledge.settings.retrievalMode.${mode}`])}
</SegmentedControlItem>
))}
</SegmentedControl>
/>
</div>
<div className="flex flex-col gap-4 sm:flex-row">

View File

@ -26,6 +26,7 @@ import { Markdown } from '@/app/components/base/markdown'
import { Link as MarkdownLink } from '@/app/components/base/markdown-blocks'
import Link from '@/next/link'
import { consoleClient, consoleQuery } from '@/service/client'
import { RetrievalModeSegmentedControl } from './components/retrieval-mode-segmented-control'
import {
extractRetrievalEvidence,
extractStreamError,
@ -1589,34 +1590,19 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
}}
/>
<div className="flex min-h-13 items-center justify-between gap-3 p-2.5">
<div
role="group"
<RetrievalModeSegmentedControl
aria-label={t(($) => $['newKnowledge.settings.retrievalModeLabel'])}
className="flex min-w-46.5 gap-0.5 rounded-lg bg-background-section-burn p-0.5"
>
{(['fast', 'deep', 'research'] as const).map((item) => (
<button
key={item}
type="button"
disabled={selectedResearchActive || localRun?.status === 'running'}
aria-pressed={mode === item}
className={cn(
'grow rounded-md px-2.5 py-1.25 system-sm-regular text-text-tertiary capitalize outline-hidden hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed',
mode === item &&
'bg-components-panel-bg font-medium text-text-primary shadow-xs',
)}
onClick={() =>
setComposerDraft({
mode: item,
query,
...(selectedHistoryKey ? { selectionKey: selectedHistoryKey } : {}),
})
}
>
{t(($) => $[`newKnowledge.settings.retrievalMode.${item}`])}
</button>
))}
</div>
appearance="composer"
disabled={selectedResearchActive || localRun?.status === 'running'}
value={mode}
onChange={(nextMode) =>
setComposerDraft({
mode: nextMode,
query,
...(selectedHistoryKey ? { selectionKey: selectedHistoryKey } : {}),
})
}
/>
<Button
variant="primary"
className="px-3.25"

View File

@ -2,6 +2,7 @@
import type { ReactNode } from 'react'
import type { NewKnowledgeSourceDraft, NewKnowledgeSourceType } from './routes'
import type { InstalledSourceProviderOption, SourceProviderOption } from './source-provider-options'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Field, FieldControl, FieldLabel } from '@langgenius/dify-ui/field'
@ -134,6 +135,99 @@ export function SourceProviderRadioGroup<T extends string>({
)
}
type SourceProviderIconValue =
| InstalledSourceProviderOption['datasource']['identity']['icon']
| InstalledSourceProviderOption['plugin']['declaration']['identity']['icon']
export function SourceProviderIcon({
fallbackIcon,
icon,
}: {
fallbackIcon: string
icon?: SourceProviderIconValue
}) {
if (typeof icon === 'string' && icon)
return <img aria-hidden alt="" className="size-4 shrink-0 object-contain" src={icon} />
if (icon && typeof icon !== 'string')
return (
<span
aria-hidden
className="flex size-4 shrink-0 items-center justify-center overflow-hidden rounded text-2xs"
style={{ backgroundColor: icon.background }}
>
{icon.content}
</span>
)
return <span aria-hidden className={`${fallbackIcon} size-4 shrink-0`} />
}
export function SourceProviderSelector({
appearance = 'page',
disabled = false,
onMoreProviders,
options,
providerKey,
onChange,
}: {
appearance?: 'embedded' | 'page'
disabled?: boolean
onMoreProviders: () => void
options: SourceProviderOption[]
providerKey: string
onChange: (providerKey: string) => void
}) {
const { t } = useTranslation('dataset')
return (
<Fieldset disabled={disabled}>
<div
className={cn(
'flex items-center justify-between gap-3',
appearance === 'embedded' ? 'mb-2' : 'mb-1.5',
)}
>
<FieldsetLegend className="py-0 system-xs-medium">
{t(($) => $['newKnowledge.providerLabel'])}
</FieldsetLegend>
<Button
type="button"
variant="ghost-accent"
size="small"
className={cn('gap-0.5', appearance === 'embedded' ? 'h-6 px-0' : 'px-2.75')}
disabled={disabled}
onClick={onMoreProviders}
>
{t(($) => $['newKnowledge.moreProviders'])}
<span aria-hidden className="i-ri-arrow-right-up-line size-3.5" />
</Button>
</div>
<SourceProviderRadioGroup
value={providerKey}
disabled={disabled}
layout="wrap"
options={options.map((option) => ({
icon: (
<SourceProviderIcon
fallbackIcon={option.fallbackIcon}
icon={
option.installed
? (option.datasource.identity.icon ?? option.plugin.declaration.identity.icon)
: undefined
}
/>
),
label: option.label,
value: option.key,
}))}
size="small"
onChange={onChange}
/>
</Fieldset>
)
}
export function SourceNameField({
className,
disabled = false,