Merge remote-tracking branch 'origin/main' into feat/model-plugins-implementing

This commit is contained in:
CodingOnStar
2026-03-17 11:28:46 +08:00
47 changed files with 380121 additions and 73 deletions

View File

@ -137,4 +137,31 @@ describe('SelectDataSet', () => {
expect(screen.getByRole('link', { name: 'appDebug.feature.dataSet.toCreate' })).toHaveAttribute('href', '/datasets/create')
expect(screen.getByRole('button', { name: 'common.operation.add' })).toBeDisabled()
})
it('uses selectedIds as the initial modal selection', async () => {
const datasetOne = makeDataset({
id: 'set-1',
name: 'Dataset One',
})
mockUseInfiniteDatasets.mockReturnValue({
data: { pages: [{ data: [datasetOne] }] },
isLoading: false,
isFetchingNextPage: false,
fetchNextPage: vi.fn(),
hasNextPage: false,
})
const onSelect = vi.fn()
await act(async () => {
render(<SelectDataSet {...baseProps} onSelect={onSelect} selectedIds={['set-1']} />)
})
expect(screen.getByText('1 appDebug.feature.dataSet.selected')).toBeInTheDocument()
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
})
expect(onSelect).toHaveBeenCalledWith([datasetOne])
})
})

View File

@ -4,7 +4,7 @@ import type { DataSet } from '@/models/datasets'
import { useInfiniteScroll } from 'ahooks'
import Link from 'next/link'
import * as React from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
@ -31,17 +31,21 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
onSelect,
}) => {
const { t } = useTranslation()
const [selected, setSelected] = useState<DataSet[]>([])
const [selectedIdsInModal, setSelectedIdsInModal] = useState(() => selectedIds)
const canSelectMulti = true
const { formatIndexingTechniqueAndMethod } = useKnowledge()
const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteDatasets(
{ page: 1 },
{ enabled: isShow, staleTime: 0, refetchOnMount: 'always' },
)
const pages = data?.pages || []
const datasets = useMemo(() => {
const pages = data?.pages || []
return pages.flatMap(page => page.data.filter(item => item.indexing_technique || item.provider === 'external'))
}, [pages])
}, [data])
const datasetMap = useMemo(() => new Map(datasets.map(item => [item.id, item])), [datasets])
const selected = useMemo(() => {
return selectedIdsInModal.map(id => datasetMap.get(id) || ({ id } as DataSet))
}, [datasetMap, selectedIdsInModal])
const hasNoData = !isLoading && datasets.length === 0
const listRef = useRef<HTMLDivElement>(null)
@ -61,50 +65,14 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
},
)
const prevSelectedIdsRef = useRef<string[]>([])
const hasUserModifiedSelectionRef = useRef(false)
useEffect(() => {
if (isShow)
hasUserModifiedSelectionRef.current = false
}, [isShow])
useEffect(() => {
const prevSelectedIds = prevSelectedIdsRef.current
const idsChanged = selectedIds.length !== prevSelectedIds.length
|| selectedIds.some((id, idx) => id !== prevSelectedIds[idx])
if (!selectedIds.length && (!hasUserModifiedSelectionRef.current || idsChanged)) {
setSelected([])
prevSelectedIdsRef.current = selectedIds
hasUserModifiedSelectionRef.current = false
return
}
if (!idsChanged && hasUserModifiedSelectionRef.current)
return
setSelected((prev) => {
const prevMap = new Map(prev.map(item => [item.id, item]))
const nextSelected = selectedIds
.map(id => datasets.find(item => item.id === id) || prevMap.get(id))
.filter(Boolean) as DataSet[]
return nextSelected
})
prevSelectedIdsRef.current = selectedIds
hasUserModifiedSelectionRef.current = false
}, [datasets, selectedIds])
const toggleSelect = (dataSet: DataSet) => {
hasUserModifiedSelectionRef.current = true
const isSelected = selected.some(item => item.id === dataSet.id)
if (isSelected) {
setSelected(selected.filter(item => item.id !== dataSet.id))
}
else {
if (canSelectMulti)
setSelected([...selected, dataSet])
else
setSelected([dataSet])
}
setSelectedIdsInModal((prev) => {
const isSelected = prev.includes(dataSet.id)
if (isSelected)
return prev.filter(id => id !== dataSet.id)
return canSelectMulti ? [...prev, dataSet.id] : [dataSet.id]
})
}
const handleSelect = () => {
@ -126,7 +94,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
{hasNoData && (
<div
className="mt-6 flex h-[128px] items-center justify-center space-x-1 rounded-lg border text-[13px]"
className="mt-6 flex h-[128px] items-center justify-center space-x-1 rounded-lg border text-[13px]"
style={{
background: 'rgba(0, 0, 0, 0.02)',
borderColor: 'rgba(0, 0, 0, 0.02',
@ -145,7 +113,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
key={item.id}
className={cn(
'flex h-10 cursor-pointer items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 shadow-xs hover:border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
selected.some(i => i.id === item.id) && 'border-[1.5px] border-components-option-card-option-selected-border bg-state-accent-hover shadow-xs hover:border-components-option-card-option-selected-border hover:bg-state-accent-hover hover:shadow-xs',
selectedIdsInModal.includes(item.id) && 'border-[1.5px] border-components-option-card-option-selected-border bg-state-accent-hover shadow-xs hover:border-components-option-card-option-selected-border hover:bg-state-accent-hover hover:shadow-xs',
!item.embedding_available && 'hover:border-components-panel-border-subtle hover:bg-components-panel-on-panel-item-bg hover:shadow-xs',
)}
onClick={() => {
@ -195,7 +163,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
)}
{!isLoading && (
<div className="mt-8 flex items-center justify-between">
<div className="text-sm font-medium text-text-secondary">
<div className="text-sm font-medium text-text-secondary">
{selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`}
</div>
<div className="flex space-x-2">

View File

@ -0,0 +1,12 @@
// This file is generated by scripts/hoist-modern-monaco.ts.
// Do not edit it manually.
export const HOIST_BASE_PATH = '/hoisted-modern-monaco' as const
export const TM_THEMES_VERSION = '1.12.1' as const
export const TM_GRAMMARS_VERSION = '1.31.2' as const
export const HOIST_THEME_IDS = ['light-plus', 'dark-plus'] as const
export const HOIST_LANGUAGE_IDS = ['javascript', 'json', 'python', 'html', 'css'] as const
export const MODERN_MONACO_IMPORT_MAP = {
'modern-monaco/editor-core': '/hoisted-modern-monaco/modern-monaco/editor-core.mjs',
'modern-monaco/lsp': '/hoisted-modern-monaco/modern-monaco/lsp/index.mjs',
} as const

View File

@ -0,0 +1,38 @@
import { headers } from 'next/headers'
import { env } from '@/env'
import { MODERN_MONACO_IMPORT_MAP } from './hoisted-config'
function withBasePath(pathname: string) {
return `${env.NEXT_PUBLIC_BASE_PATH}${pathname}`
}
function getRequestOrigin(requestHeaders: Headers) {
const protocol = requestHeaders.get('x-forwarded-proto') ?? 'http'
const host = requestHeaders.get('x-forwarded-host') ?? requestHeaders.get('host')
if (!host)
return null
return `${protocol}://${host}`
}
const MonacoImportMap = async () => {
const requestHeaders = await headers()
const nonce = process.env.NODE_ENV === 'production' ? requestHeaders.get('x-nonce') ?? '' : ''
const requestOrigin = getRequestOrigin(requestHeaders)
const importMap = JSON.stringify({
imports: Object.fromEntries(
Object.entries(MODERN_MONACO_IMPORT_MAP).map(([specifier, pathname]) => {
const modulePath = withBasePath(pathname)
const moduleUrl = requestOrigin ? new URL(modulePath, requestOrigin).toString() : modulePath
return [specifier, moduleUrl]
}),
),
})
return (
<script nonce={nonce || undefined} type="importmap" data-modern-monaco-importmap="">
{importMap}
</script>
)
}
export default MonacoImportMap

View File

@ -1,14 +1,24 @@
import type { InitOptions } from 'modern-monaco'
import { basePath } from '@/utils/var'
import {
HOIST_BASE_PATH,
HOIST_LANGUAGE_IDS,
HOIST_THEME_IDS,
TM_GRAMMARS_VERSION,
TM_THEMES_VERSION,
} from './hoisted-config'
export const LIGHT_THEME_ID = 'light-plus'
export const DARK_THEME_ID = 'dark-plus'
const assetPath = (pathname: string) => `${basePath}${HOIST_BASE_PATH}${pathname}`
const themeAssetPath = (themeId: string) => assetPath(`/tm-themes@${TM_THEMES_VERSION}/themes/${themeId}.json`)
const grammarAssetPath = (languageId: string) => assetPath(`/tm-grammars@${TM_GRAMMARS_VERSION}/grammars/${languageId}.json`)
const DEFAULT_INIT_OPTIONS: InitOptions = {
defaultTheme: DARK_THEME_ID,
themes: [
LIGHT_THEME_ID,
DARK_THEME_ID,
],
defaultTheme: themeAssetPath(DARK_THEME_ID),
themes: HOIST_THEME_IDS.map(themeAssetPath),
langs: HOIST_LANGUAGE_IDS.map(grammarAssetPath),
}
let monacoInitPromise: Promise<typeof import('modern-monaco/editor-core') | null> | null = null

View File

@ -137,5 +137,11 @@ describe('ScoreThresholdItem', () => {
const input = screen.getByRole('textbox')
expect(input).toHaveValue('1')
})
it('should fall back to default value when value is undefined', () => {
render(<ScoreThresholdItem {...defaultProps} value={undefined} />)
const input = screen.getByRole('textbox')
expect(input).toHaveValue('0.7')
})
})
})

View File

@ -6,7 +6,7 @@ import ParamItem from '.'
type Props = {
className?: string
value: number
value?: number
onChange: (key: string, value: number) => void
enable: boolean
hasSwitch?: boolean
@ -20,6 +20,18 @@ const VALUE_LIMIT = {
max: 1,
}
const normalizeScoreThreshold = (value?: number): number => {
const normalizedValue = typeof value === 'number' && Number.isFinite(value)
? value
: VALUE_LIMIT.default
const roundedValue = Number.parseFloat(normalizedValue.toFixed(2))
return Math.min(
VALUE_LIMIT.max,
Math.max(VALUE_LIMIT.min, roundedValue),
)
}
const ScoreThresholdItem: FC<Props> = ({
className,
value,
@ -29,16 +41,10 @@ const ScoreThresholdItem: FC<Props> = ({
onSwitchChange,
}) => {
const { t } = useTranslation()
const handleParamChange = (key: string, value: number) => {
let notOutRangeValue = Number.parseFloat(value.toFixed(2))
notOutRangeValue = Math.max(VALUE_LIMIT.min, notOutRangeValue)
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
onChange(key, notOutRangeValue)
const handleParamChange = (key: string, nextValue: number) => {
onChange(key, normalizeScoreThreshold(nextValue))
}
const safeValue = Math.min(
VALUE_LIMIT.max,
Math.max(VALUE_LIMIT.min, Number.parseFloat(value.toFixed(2))),
)
const safeValue = normalizeScoreThreshold(value)
return (
<ParamItem

View File

@ -203,7 +203,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
<PortalToFollowElemContent className="z-[1002]">
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
<Tabs
tabs={tabs}

View File

@ -169,7 +169,7 @@ const ToolPicker: FC<Props> = ({
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className="z-[1000]">
<PortalToFollowElemContent className="z-[1002]">
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}>
<div className="p-2 pb-1">
<SearchBox

View File

@ -8,6 +8,7 @@ import GlobalPublicStoreProvider from '@/context/global-public-context'
import { TanstackQueryInitializer } from '@/context/query-client'
import { getDatasetMap } from '@/env'
import { getLocaleOnServer } from '@/i18n-config/server'
import MonacoImportMap from './components/base/modern-monaco/import-map'
import { ToastProvider } from './components/base/toast'
import { TooltipProvider } from './components/base/ui/tooltip'
import BrowserInitializer from './components/browser-initializer'
@ -37,6 +38,7 @@ const LocaleLayout = async ({
return (
<html lang={locale ?? 'en'} className="h-full" suppressHydrationWarning>
<head>
<MonacoImportMap />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#1C64F2" />
<meta name="mobile-web-app-capable" content="yes" />