Merge branch 'main' into refactor/query-params-nuqs

This commit is contained in:
yyh
2026-01-05 15:03:41 +08:00
committed by GitHub
279 changed files with 31743 additions and 3006 deletions

View File

@ -7,8 +7,12 @@ logs
# node
node_modules
dist
build
coverage
.husky
.next
.pnpm-store
# vscode
.vscode
@ -22,3 +26,7 @@ node_modules
# Jetbrains
.idea
# git
.git
.gitignore

View File

@ -47,6 +47,8 @@ NEXT_PUBLIC_TOP_K_MAX_VALUE=10
# The maximum number of tokens for segmentation
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
# Used by web/docker/entrypoint.sh to overwrite/export NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH at container startup (Docker only)
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
# Maximum loop count in the workflow
NEXT_PUBLIC_LOOP_NODE_MAX_COUNT=100

View File

@ -12,7 +12,8 @@ RUN apk add --no-cache tzdata
RUN corepack enable
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV NEXT_PUBLIC_BASE_PATH=""
ARG NEXT_PUBLIC_BASE_PATH=""
ENV NEXT_PUBLIC_BASE_PATH="$NEXT_PUBLIC_BASE_PATH"
# install packages

View File

@ -7,19 +7,24 @@ export const jsonObjectWrap = {
export const jsonConfigPlaceHolder = JSON.stringify(
{
foo: {
type: 'string',
},
bar: {
type: 'object',
properties: {
sub: {
type: 'number',
},
type: 'object',
properties: {
foo: {
type: 'string',
},
bar: {
type: 'object',
properties: {
sub: {
type: 'number',
},
},
required: [],
additionalProperties: true,
},
required: [],
additionalProperties: true,
},
required: [],
additionalProperties: true,
},
null,
2,

View File

@ -28,7 +28,7 @@ import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInpu
import ConfigSelect from '../config-select'
import ConfigString from '../config-string'
import ModalFoot from '../modal-foot'
import { jsonConfigPlaceHolder, jsonObjectWrap } from './config'
import { jsonConfigPlaceHolder } from './config'
import Field from './field'
import TypeSelector from './type-select'
@ -78,13 +78,12 @@ const ConfigModal: FC<IConfigModalProps> = ({
const modalRef = useRef<HTMLDivElement>(null)
const appDetail = useAppStore(state => state.appDetail)
const isBasicApp = appDetail?.mode !== AppModeEnum.ADVANCED_CHAT && appDetail?.mode !== AppModeEnum.WORKFLOW
const isSupportJSON = false
const jsonSchemaStr = useMemo(() => {
const isJsonObject = type === InputVarType.jsonObject
if (!isJsonObject || !tempPayload.json_schema)
return ''
try {
return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2)
return JSON.stringify(JSON.parse(tempPayload.json_schema), null, 2)
}
catch {
return ''
@ -129,13 +128,14 @@ const ConfigModal: FC<IConfigModalProps> = ({
}, [])
const handleJSONSchemaChange = useCallback((value: string) => {
const isEmpty = value == null || value.trim() === ''
if (isEmpty) {
handlePayloadChange('json_schema')(undefined)
return null
}
try {
const v = JSON.parse(value)
const res = {
...jsonObjectWrap,
properties: v,
}
handlePayloadChange('json_schema')(JSON.stringify(res, null, 2))
handlePayloadChange('json_schema')(JSON.stringify(v, null, 2))
}
catch {
return null
@ -175,7 +175,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
},
]
: []),
...((!isBasicApp && isSupportJSON)
...((!isBasicApp)
? [{
name: t('variableConfig.json', { ns: 'appDebug' }),
value: InputVarType.jsonObject,
@ -233,7 +233,28 @@ const ConfigModal: FC<IConfigModalProps> = ({
const checkboxDefaultSelectValue = useMemo(() => getCheckboxDefaultSelectValue(tempPayload.default), [tempPayload.default])
const isJsonSchemaEmpty = (value: InputVar['json_schema']) => {
if (value === null || value === undefined) {
return true
}
if (typeof value !== 'string') {
return false
}
const trimmed = value.trim()
return trimmed === ''
}
const handleConfirm = () => {
const jsonSchemaValue = tempPayload.json_schema
const isSchemaEmpty = isJsonSchemaEmpty(jsonSchemaValue)
const normalizedJsonSchema = isSchemaEmpty ? undefined : jsonSchemaValue
// if the input type is jsonObject and the schema is empty as determined by `isJsonSchemaEmpty`,
// remove the `json_schema` field from the payload by setting its value to `undefined`.
const payloadToSave = tempPayload.type === InputVarType.jsonObject && isSchemaEmpty
? { ...tempPayload, json_schema: undefined }
: tempPayload
const moreInfo = tempPayload.variable === payload?.variable
? undefined
: {
@ -250,7 +271,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
return
}
if (isStringInput || type === InputVarType.number) {
onConfirm(tempPayload, moreInfo)
onConfirm(payloadToSave, moreInfo)
}
else if (type === InputVarType.select) {
if (options?.length === 0) {
@ -270,7 +291,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }) })
return
}
onConfirm(tempPayload, moreInfo)
onConfirm(payloadToSave, moreInfo)
}
else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
if (tempPayload.allowed_file_types?.length === 0) {
@ -283,10 +304,26 @@ const ConfigModal: FC<IConfigModalProps> = ({
Toast.notify({ type: 'error', message: errorMessages })
return
}
onConfirm(tempPayload, moreInfo)
onConfirm(payloadToSave, moreInfo)
}
else if (type === InputVarType.jsonObject) {
if (!isSchemaEmpty && typeof normalizedJsonSchema === 'string') {
try {
const schema = JSON.parse(normalizedJsonSchema)
if (schema?.type !== 'object') {
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' }) })
return
}
}
catch {
Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' }) })
return
}
}
onConfirm(payloadToSave, moreInfo)
}
else {
onConfirm(tempPayload, moreInfo)
onConfirm(payloadToSave, moreInfo)
}
}

View File

@ -5,15 +5,6 @@ import * as React from 'react'
import { AgentStrategy } from '@/types/app'
import AgentSettingButton from './agent-setting-button'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))
let latestAgentSettingProps: any
vi.mock('./agent/agent-setting', () => ({
default: (props: any) => {

View File

@ -15,15 +15,6 @@ vi.mock('use-context-selector', async (importOriginal) => {
}
})
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))
const mockUseFeatures = vi.fn()
const mockUseFeaturesStore = vi.fn()
vi.mock('@/app/components/base/features/hooks', () => ({

View File

@ -93,7 +93,6 @@ function createMockProviderContext(overrides: Partial<ProviderContextState> = {}
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [
{
@ -711,7 +710,6 @@ describe('DebugWithSingleModel', () => {
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [
{
@ -742,7 +740,6 @@ describe('DebugWithSingleModel', () => {
provider: 'different-provider',
label: { en_US: 'Different Provider', zh_Hans: '不同提供商' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [],
},
@ -925,7 +922,6 @@ describe('DebugWithSingleModel', () => {
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [
{
@ -975,7 +971,6 @@ describe('DebugWithSingleModel', () => {
provider: 'openai',
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
icon_large: { en_US: 'icon', zh_Hans: 'icon' },
status: ModelStatusEnum.active,
models: [
{

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './OpenaiBlue.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiBlue'
export default Icon

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './OpenaiTeal.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiTeal'
export default Icon

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './OpenaiViolet.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiViolet'
export default Icon

View File

@ -26,12 +26,9 @@ export { default as Localai } from './Localai'
export { default as LocalaiText } from './LocalaiText'
export { default as Microsoft } from './Microsoft'
export { default as OpenaiBlack } from './OpenaiBlack'
export { default as OpenaiBlue } from './OpenaiBlue'
export { default as OpenaiGreen } from './OpenaiGreen'
export { default as OpenaiTeal } from './OpenaiTeal'
export { default as OpenaiText } from './OpenaiText'
export { default as OpenaiTransparent } from './OpenaiTransparent'
export { default as OpenaiViolet } from './OpenaiViolet'
export { default as OpenaiYellow } from './OpenaiYellow'
export { default as Openllm } from './Openllm'
export { default as OpenllmText } from './OpenllmText'

View File

@ -1,26 +1,14 @@
import { cleanup, fireEvent, render } from '@testing-library/react'
import * as React from 'react'
import { createReactI18nextMock } from '@/test/i18n-mock'
import InlineDeleteConfirm from './index'
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, defaultValueOrOptions?: string | { ns?: string }) => {
const translations: Record<string, string> = {
'operation.deleteConfirmTitle': 'Delete?',
'operation.yes': 'Yes',
'operation.no': 'No',
'operation.confirmAction': 'Please confirm your action.',
}
if (translations[key])
return translations[key]
// Handle case where second arg is default value string
if (typeof defaultValueOrOptions === 'string')
return defaultValueOrOptions
const prefix = defaultValueOrOptions?.ns ? `${defaultValueOrOptions.ns}.` : ''
return `${prefix}${key}`
},
}),
// Mock react-i18next with custom translations for test assertions
vi.mock('react-i18next', () => createReactI18nextMock({
'operation.deleteConfirmTitle': 'Delete?',
'operation.yes': 'Yes',
'operation.no': 'No',
'operation.confirmAction': 'Please confirm your action.',
}))
afterEach(cleanup)

View File

@ -1,5 +1,6 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { createReactI18nextMock } from '@/test/i18n-mock'
import InputWithCopy from './index'
// Create a mock function that we can track using vi.hoisted
@ -10,22 +11,12 @@ vi.mock('copy-to-clipboard', () => ({
default: mockCopyToClipboard,
}))
// Mock the i18n hook
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const translations: Record<string, string> = {
'operation.copy': 'Copy',
'operation.copied': 'Copied',
'overview.appInfo.embedded.copy': 'Copy',
'overview.appInfo.embedded.copied': 'Copied',
}
if (translations[key])
return translations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
// Mock the i18n hook with custom translations for test assertions
vi.mock('react-i18next', () => createReactI18nextMock({
'operation.copy': 'Copy',
'operation.copied': 'Copied',
'overview.appInfo.embedded.copy': 'Copy',
'overview.appInfo.embedded.copied': 'Copied',
}))
// Mock es-toolkit/compat debounce

View File

@ -1,21 +1,12 @@
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { createReactI18nextMock } from '@/test/i18n-mock'
import Input, { inputVariants } from './index'
// Mock the i18n hook
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const translations: Record<string, string> = {
'operation.search': 'Search',
'placeholder.input': 'Please input',
}
if (translations[key])
return translations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
// Mock the i18n hook with custom translations for test assertions
vi.mock('react-i18next', () => createReactI18nextMock({
'operation.search': 'Search',
'placeholder.input': 'Please input',
}))
describe('Input component', () => {

View File

@ -0,0 +1,248 @@
import { act, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ChatContextProvider } from '@/app/components/base/chat/chat/context'
import ThinkBlock from './think-block'
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'chat.thinking': 'Thinking...',
'chat.thought': 'Thought',
}
return translations[key] || key
},
}),
}))
// Helper to wrap component with ChatContextProvider
const renderWithContext = (
children: React.ReactNode,
isResponding: boolean = true,
) => {
return render(
<ChatContextProvider
config={undefined}
isResponding={isResponding}
chatList={[]}
showPromptLog={false}
questionIcon={undefined}
answerIcon={undefined}
onSend={undefined}
onRegenerate={undefined}
onAnnotationEdited={undefined}
onAnnotationAdded={undefined}
onAnnotationRemoved={undefined}
onFeedback={undefined}
>
{children}
</ChatContextProvider>,
)
}
describe('ThinkBlock', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
describe('Rendering', () => {
it('should render regular details element when data-think is false', () => {
render(
<ThinkBlock data-think={false}>
<p>Regular content</p>
</ThinkBlock>,
)
expect(screen.getByText('Regular content')).toBeInTheDocument()
})
it('should render think block with thinking state when data-think is true', () => {
renderWithContext(
<ThinkBlock data-think={true}>
<p>Thinking content</p>
</ThinkBlock>,
true,
)
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
expect(screen.getByText('Thinking content')).toBeInTheDocument()
})
it('should render thought state when content has ENDTHINKFLAG', () => {
renderWithContext(
<ThinkBlock data-think={true}>
<p>Completed thinking[ENDTHINKFLAG]</p>
</ThinkBlock>,
true,
)
expect(screen.getByText(/Thought/)).toBeInTheDocument()
})
})
describe('Timer behavior', () => {
it('should update elapsed time while thinking', () => {
renderWithContext(
<ThinkBlock data-think={true}>
<p>Thinking...</p>
</ThinkBlock>,
true,
)
// Initial state should show 0.0s
expect(screen.getByText(/\(0\.0s\)/)).toBeInTheDocument()
// Advance timer by 500ms and run pending timers
act(() => {
vi.advanceTimersByTime(500)
})
// Should show approximately 0.5s
expect(screen.getByText(/\(0\.5s\)/)).toBeInTheDocument()
})
it('should stop timer when isResponding becomes false', () => {
const { rerender } = render(
<ChatContextProvider
config={undefined}
isResponding={true}
chatList={[]}
showPromptLog={false}
questionIcon={undefined}
answerIcon={undefined}
onSend={undefined}
onRegenerate={undefined}
onAnnotationEdited={undefined}
onAnnotationAdded={undefined}
onAnnotationRemoved={undefined}
onFeedback={undefined}
>
<ThinkBlock data-think={true}>
<p>Thinking content</p>
</ThinkBlock>
</ChatContextProvider>,
)
// Verify initial thinking state
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
// Advance timer
act(() => {
vi.advanceTimersByTime(1000)
})
// Simulate user clicking stop (isResponding becomes false)
rerender(
<ChatContextProvider
config={undefined}
isResponding={false}
chatList={[]}
showPromptLog={false}
questionIcon={undefined}
answerIcon={undefined}
onSend={undefined}
onRegenerate={undefined}
onAnnotationEdited={undefined}
onAnnotationAdded={undefined}
onAnnotationRemoved={undefined}
onFeedback={undefined}
>
<ThinkBlock data-think={true}>
<p>Thinking content</p>
</ThinkBlock>
</ChatContextProvider>,
)
// Should now show "Thought" instead of "Thinking..."
expect(screen.getByText(/Thought/)).toBeInTheDocument()
})
it('should NOT stop timer when isResponding is undefined (outside ChatContextProvider)', () => {
// Render without ChatContextProvider
render(
<ThinkBlock data-think={true}>
<p>Content without ENDTHINKFLAG</p>
</ThinkBlock>,
)
// Initial state should show "Thinking..."
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
// Advance timer
act(() => {
vi.advanceTimersByTime(2000)
})
// Timer should still be running (showing "Thinking..." not "Thought")
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
expect(screen.getByText(/\(2\.0s\)/)).toBeInTheDocument()
})
})
describe('ENDTHINKFLAG handling', () => {
it('should remove ENDTHINKFLAG from displayed content', () => {
renderWithContext(
<ThinkBlock data-think={true}>
<p>Content[ENDTHINKFLAG]</p>
</ThinkBlock>,
true,
)
expect(screen.getByText('Content')).toBeInTheDocument()
expect(screen.queryByText('[ENDTHINKFLAG]')).not.toBeInTheDocument()
})
it('should detect ENDTHINKFLAG in nested children', () => {
renderWithContext(
<ThinkBlock data-think={true}>
<div>
<span>Nested content[ENDTHINKFLAG]</span>
</div>
</ThinkBlock>,
true,
)
// Should show "Thought" since ENDTHINKFLAG is present
expect(screen.getByText(/Thought/)).toBeInTheDocument()
})
it('should detect ENDTHINKFLAG in array children', () => {
renderWithContext(
<ThinkBlock data-think={true}>
{['Part 1', 'Part 2[ENDTHINKFLAG]']}
</ThinkBlock>,
true,
)
expect(screen.getByText(/Thought/)).toBeInTheDocument()
})
})
describe('Edge cases', () => {
it('should handle empty children', () => {
renderWithContext(
<ThinkBlock data-think={true}></ThinkBlock>,
true,
)
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
})
it('should handle null children gracefully', () => {
renderWithContext(
<ThinkBlock data-think={true}>
{null}
</ThinkBlock>,
true,
)
expect(screen.getByText(/Thinking\.\.\./)).toBeInTheDocument()
})
})
})

View File

@ -59,7 +59,11 @@ const useThinkTimer = (children: any) => {
}, [startTime, isComplete])
useEffect(() => {
if (hasEndThink(children) || !isResponding)
// Stop timer when:
// 1. Content has [ENDTHINKFLAG] marker (normal completion)
// 2. isResponding is explicitly false (user clicked stop button)
// Note: Don't stop when isResponding is undefined (component used outside ChatContextProvider)
if (hasEndThink(children) || isResponding === false)
setIsComplete(true)
}, [children, isResponding])

View File

@ -3,8 +3,6 @@ import * as React from 'react'
import { CategoryEnum } from '.'
import Footer from './footer'
let mockTranslations: Record<string, string> = {}
vi.mock('next/link', () => ({
default: ({ children, href, className, target }: { children: React.ReactNode, href: string, className?: string, target?: string }) => (
<a href={href} className={className} target={target} data-testid="pricing-link">
@ -13,25 +11,9 @@ vi.mock('next/link', () => ({
),
}))
vi.mock('react-i18next', async (importOriginal) => {
const actual = await importOriginal<typeof import('react-i18next')>()
return {
...actual,
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
if (mockTranslations[key])
return mockTranslations[key]
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}
})
describe('Footer', () => {
beforeEach(() => {
vi.clearAllMocks()
mockTranslations = {}
})
// Rendering behavior

View File

@ -18,16 +18,6 @@ const IndexingTypeValues = {
// Mock External Dependencies
// ==========================================
// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages)
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))
// Mock next/link
vi.mock('next/link', () => {
return function MockLink({ children, href }: { children: React.ReactNode, href: string }) {

View File

@ -9,16 +9,6 @@ import Processing from './index'
// Mock External Dependencies
// ==========================================
// Mock react-i18next (handled by global mock in web/vitest.setup.ts but we override for custom messages)
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
const prefix = options?.ns ? `${options.ns}.` : ''
return `${prefix}${key}`
},
}),
}))
// Mock useDocLink - returns a function that generates doc URLs
// Strips leading slash from path to match actual implementation behavior
vi.mock('@/context/i18n', () => ({

View File

@ -110,7 +110,7 @@ const GotoAnything: FC<Props> = ({
isWorkflowPage,
isRagPipelinePage,
defaultLocale,
Object.keys(Actions).sort().join(','),
Actions,
],
queryFn: async () => {
const query = searchQueryDebouncedValue.toLowerCase()

View File

@ -218,7 +218,6 @@ export type ModelProvider = {
}
icon_small: TypeWithI18N
icon_small_dark?: TypeWithI18N
icon_large: TypeWithI18N
background?: string
supported_model_types: ModelTypeEnum[]
configurate_methods: ConfigurationMethodEnum[]
@ -254,7 +253,6 @@ export type ModelProvider = {
export type Model = {
provider: string
icon_large: TypeWithI18N
icon_small: TypeWithI18N
icon_small_dark?: TypeWithI18N
label: TypeWithI18N
@ -267,7 +265,6 @@ export type DefaultModelResponse = {
model_type: ModelTypeEnum
provider: {
provider: string
icon_large: TypeWithI18N
icon_small: TypeWithI18N
}
}

View File

@ -3,7 +3,7 @@ import type {
Model,
ModelProvider,
} from '../declarations'
import { OpenaiBlue, OpenaiTeal, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
import { OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
import { Group } from '@/app/components/base/icons/src/vender/other'
import useTheme from '@/hooks/use-theme'
import { renderI18nObject } from '@/i18n-config'
@ -29,12 +29,6 @@ const ModelIcon: FC<ModelIconProps> = ({
const language = useLanguage()
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o'))
return <div className="flex items-center justify-center"><OpenaiYellow className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4.1'))
return <div className="flex items-center justify-center"><OpenaiTeal className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4o'))
return <div className="flex items-center justify-center"><OpenaiBlue className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('gpt-4'))
return <div className="flex items-center justify-center"><OpenaiViolet className={cn('h-5 w-5', className)} /></div>
if (provider?.icon_small) {
return (

View File

@ -21,33 +21,6 @@ import Card from './index'
// Mock External Dependencies Only
// ================================
// Mock react-i18next (translation hook)
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock useMixedTranslation hook
vi.mock('../marketplace/hooks', () => ({
useMixedTranslation: (_locale?: string) => ({
t: (key: string, options?: { ns?: string }) => {
const fullKey = options?.ns ? `${options.ns}.${key}` : key
const translations: Record<string, string> = {
'plugin.marketplace.partnerTip': 'Partner plugin',
'plugin.marketplace.verifiedTip': 'Verified plugin',
'plugin.installModal.installWarning': 'Install warning message',
}
return translations[fullKey] || key
},
}),
}))
// Mock useGetLanguage context
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
}))
// Mock useTheme hook
vi.mock('@/hooks/use-theme', () => ({
default: () => ({ theme: 'light' }),

View File

@ -64,26 +64,20 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
// Handle interpolation params (excluding ns)
const { ns: _ns, ...params } = options || {}
if (Object.keys(params).length > 0) {
return `${fullKey}:${JSON.stringify(params)}`
}
return fullKey
},
}),
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
<span data-testid="trans">
{i18nKey}
{components?.trustSource}
</span>
),
}))
vi.mock('react-i18next', async (importOriginal) => {
const actual = await importOriginal<typeof import('react-i18next')>()
const { createReactI18nextMock } = await import('@/test/i18n-mock')
return {
...actual,
...createReactI18nextMock(),
Trans: ({ i18nKey, components }: { i18nKey: string, components?: Record<string, React.ReactNode> }) => (
<span data-testid="trans">
{i18nKey}
{components?.trustSource}
</span>
),
}
})
vi.mock('../../../card', () => ({
default: ({ payload, titleLeft }: {

View File

@ -48,21 +48,6 @@ vi.mock('@/service/plugins', () => ({
uploadFile: (...args: unknown[]) => mockUploadFile(...args),
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string } & Record<string, unknown>) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
// Handle interpolation params (excluding ns)
const { ns: _ns, ...params } = options || {}
if (Object.keys(params).length > 0) {
return `${fullKey}:${JSON.stringify(params)}`
}
return fullKey
},
}),
}))
vi.mock('../../../card', () => ({
default: ({ payload, isLoading, loadingFileName }: {
payload: { name: string }

View File

@ -27,17 +27,17 @@ import {
// Mock External Dependencies Only
// ================================
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock i18next-config
vi.mock('@/i18n-config/i18next-config', () => ({
default: {
getFixedT: (_locale: string) => (key: string) => key,
getFixedT: (_locale: string) => (key: string, options?: Record<string, unknown>) => {
if (options && options.ns) {
return `${options.ns}.${key}`
}
else {
return key
}
},
},
}))
@ -617,8 +617,8 @@ describe('hooks', () => {
it('should return translation key when no translation found', () => {
const { result } = renderHook(() => useMixedTranslation())
// The mock returns key as-is
expect(result.current.t('category.all', { ns: 'plugin' })).toBe('category.all')
// The global mock returns key with namespace prefix
expect(result.current.t('category.all', { ns: 'plugin' })).toBe('plugin.category.all')
})
it('should use locale from outer when provided', () => {
@ -638,8 +638,8 @@ describe('hooks', () => {
it('should use getFixedT when localeFromOuter is provided', () => {
const { result } = renderHook(() => useMixedTranslation('fr-FR'))
// Should still return a function
expect(result.current.t('search', { ns: 'plugin' })).toBe('search')
// The global mock returns key with namespace prefix
expect(result.current.t('search', { ns: 'plugin' })).toBe('plugin.search')
})
})
})
@ -2756,15 +2756,15 @@ describe('PluginTypeSwitch Component', () => {
</MarketplaceContextProvider>,
)
// Note: The mock returns the key without namespace prefix
expect(screen.getByText('category.all')).toBeInTheDocument()
expect(screen.getByText('category.models')).toBeInTheDocument()
expect(screen.getByText('category.tools')).toBeInTheDocument()
expect(screen.getByText('category.datasources')).toBeInTheDocument()
expect(screen.getByText('category.triggers')).toBeInTheDocument()
expect(screen.getByText('category.agents')).toBeInTheDocument()
expect(screen.getByText('category.extensions')).toBeInTheDocument()
expect(screen.getByText('category.bundles')).toBeInTheDocument()
// Note: The global mock returns the key with namespace prefix (plugin.)
expect(screen.getByText('plugin.category.all')).toBeInTheDocument()
expect(screen.getByText('plugin.category.models')).toBeInTheDocument()
expect(screen.getByText('plugin.category.tools')).toBeInTheDocument()
expect(screen.getByText('plugin.category.datasources')).toBeInTheDocument()
expect(screen.getByText('plugin.category.triggers')).toBeInTheDocument()
expect(screen.getByText('plugin.category.agents')).toBeInTheDocument()
expect(screen.getByText('plugin.category.extensions')).toBeInTheDocument()
expect(screen.getByText('plugin.category.bundles')).toBeInTheDocument()
})
it('should apply className prop', () => {
@ -2794,7 +2794,7 @@ describe('PluginTypeSwitch Component', () => {
</MarketplaceContextProvider>,
)
fireEvent.click(screen.getByText('category.tools'))
fireEvent.click(screen.getByText('plugin.category.tools'))
expect(screen.getByTestId('active-type-display')).toHaveTextContent('tool')
})
@ -2816,7 +2816,7 @@ describe('PluginTypeSwitch Component', () => {
)
fireEvent.click(screen.getByTestId('set-model'))
const modelOption = screen.getByText('category.models').closest('div')
const modelOption = screen.getByText('plugin.category.models').closest('div')
expect(modelOption).toHaveClass('shadow-xs')
})
})

View File

@ -219,7 +219,6 @@ const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
*/
const createModel = (overrides: Partial<Model> = {}): Model => ({
provider: 'openai',
icon_large: { en_US: 'icon-large.png', zh_Hans: 'icon-large.png' },
icon_small: { en_US: 'icon-small.png', zh_Hans: 'icon-small.png' },
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
models: [createModelItem()],

View File

@ -78,17 +78,6 @@ function createMockLogData(logs: TriggerLogEntity[] = []): { logs: TriggerLogEnt
// Mock Setup
// ============================================================================
const mockTranslate = vi.fn((key: string, options?: { ns?: string }) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
return fullKey
})
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: mockTranslate,
}),
}))
// Mock plugin store
const mockPluginDetail = createMockPluginDetail()
const mockUsePluginStore = vi.fn(() => mockPluginDetail)

View File

@ -68,17 +68,6 @@ function createMockSubscriptionBuilder(overrides: Partial<TriggerSubscriptionBui
// Mock Setup
// ============================================================================
const mockTranslate = vi.fn((key: string, options?: { ns?: string }) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
return fullKey
})
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: mockTranslate,
}),
}))
// Mock plugin store
const mockPluginDetail = createMockPluginDetail()
const mockUsePluginStore = vi.fn(() => mockPluginDetail)

View File

@ -12,16 +12,6 @@ import { OAuthEditModal } from './oauth-edit-modal'
// ==================== Mock Setup ====================
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
return fullKey
},
}),
}))
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
default: { notify: (params: unknown) => mockToastNotify(params) },

View File

@ -9,28 +9,6 @@ import PluginMutationModal from './index'
// Mock External Dependencies Only
// ================================
// Mock react-i18next (translation hook)
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock useMixedTranslation hook
vi.mock('../marketplace/hooks', () => ({
useMixedTranslation: (_locale?: string) => ({
t: (key: string, options?: { ns?: string }) => {
const fullKey = options?.ns ? `${options.ns}.${key}` : key
return fullKey
},
}),
}))
// Mock useGetLanguage context
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
}))
// Mock useTheme hook
vi.mock('@/hooks/use-theme', () => ({
default: () => ({ theme: 'light' }),

View File

@ -1,10 +1,13 @@
'use client'
import type { PluginDetail } from '../types'
import type { FilterState } from './filter-management'
import { useDebounceFn } from 'ahooks'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { useGetLanguage } from '@/context/i18n'
import { renderI18nObject } from '@/i18n-config'
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import Loading from '../../base/loading'
import { PluginSource } from '../types'
@ -13,8 +16,34 @@ import Empty from './empty'
import FilterManagement from './filter-management'
import List from './list'
const matchesSearchQuery = (plugin: PluginDetail & { latest_version: string }, query: string, locale: string): boolean => {
if (!query)
return true
const lowerQuery = query.toLowerCase()
const { declaration } = plugin
// Match plugin_id
if (plugin.plugin_id.toLowerCase().includes(lowerQuery))
return true
// Match plugin name
if (plugin.name?.toLowerCase().includes(lowerQuery))
return true
// Match declaration name
if (declaration.name?.toLowerCase().includes(lowerQuery))
return true
// Match localized label
const label = renderI18nObject(declaration.label, locale)
if (label?.toLowerCase().includes(lowerQuery))
return true
// Match localized description
const description = renderI18nObject(declaration.description, locale)
if (description?.toLowerCase().includes(lowerQuery))
return true
return false
}
const PluginsPanel = () => {
const { t } = useTranslation()
const locale = useGetLanguage()
const filters = usePluginPageContext(v => v.filters) as FilterState
const setFilters = usePluginPageContext(v => v.setFilters)
const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginList()
@ -48,11 +77,11 @@ const PluginsPanel = () => {
return (
(categories.length === 0 || categories.includes(plugin.declaration.category))
&& (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag)))
&& (searchQuery === '' || plugin.plugin_id.toLowerCase().includes(searchQuery.toLowerCase()))
&& matchesSearchQuery(plugin, searchQuery, locale)
)
})
return filteredList
}, [pluginListWithLatestVersion, filters])
}, [pluginListWithLatestVersion, filters, locale])
const currentPluginDetail = useMemo(() => {
const detail = pluginListWithLatestVersion.find(plugin => plugin.plugin_id === currentPluginID)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,971 @@
import type { PanelProps } from '@/app/components/workflow/panel'
import { render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import RagPipelinePanel from './index'
// ============================================================================
// Mock External Dependencies
// ============================================================================
// Type definitions for dynamic module
type DynamicModule = {
default?: React.ComponentType<Record<string, unknown>>
}
type PromiseOrModule = Promise<DynamicModule> | DynamicModule
// Mock next/dynamic to return synchronous components immediately
vi.mock('next/dynamic', () => ({
default: (loader: () => PromiseOrModule, _options?: Record<string, unknown>) => {
let Component: React.ComponentType<Record<string, unknown>> | null = null
// Try to resolve the loader synchronously for mocked modules
try {
const result = loader() as PromiseOrModule
if (result && typeof (result as Promise<DynamicModule>).then === 'function') {
// For async modules, we need to handle them specially
// This will work with vi.mock since mocks resolve synchronously
(result as Promise<DynamicModule>).then((mod: DynamicModule) => {
Component = (mod.default || mod) as React.ComponentType<Record<string, unknown>>
})
}
else if (result) {
Component = ((result as DynamicModule).default || result) as React.ComponentType<Record<string, unknown>>
}
}
catch {
// If the module can't be resolved, Component stays null
}
// Return a simple wrapper that renders the component or null
const DynamicComponent = React.forwardRef((props: Record<string, unknown>, ref: React.Ref<unknown>) => {
// For mocked modules, Component should already be set
if (Component)
return <Component {...props} ref={ref} />
return null
})
DynamicComponent.displayName = 'DynamicComponent'
return DynamicComponent
},
}))
// Mock workflow store
let mockHistoryWorkflowData: Record<string, unknown> | null = null
let mockShowDebugAndPreviewPanel = false
let mockShowGlobalVariablePanel = false
let mockShowInputFieldPanel = false
let mockShowInputFieldPreviewPanel = false
let mockInputFieldEditPanelProps: Record<string, unknown> | null = null
let mockPipelineId = 'test-pipeline-123'
type MockStoreState = {
historyWorkflowData: Record<string, unknown> | null
showDebugAndPreviewPanel: boolean
showGlobalVariablePanel: boolean
showInputFieldPanel: boolean
showInputFieldPreviewPanel: boolean
inputFieldEditPanelProps: Record<string, unknown> | null
pipelineId: string
}
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: MockStoreState) => unknown) => {
const state: MockStoreState = {
historyWorkflowData: mockHistoryWorkflowData,
showDebugAndPreviewPanel: mockShowDebugAndPreviewPanel,
showGlobalVariablePanel: mockShowGlobalVariablePanel,
showInputFieldPanel: mockShowInputFieldPanel,
showInputFieldPreviewPanel: mockShowInputFieldPreviewPanel,
inputFieldEditPanelProps: mockInputFieldEditPanelProps,
pipelineId: mockPipelineId,
}
return selector(state)
},
}))
// Mock Panel component to capture props and render children
let capturedPanelProps: PanelProps | null = null
vi.mock('@/app/components/workflow/panel', () => ({
default: (props: PanelProps) => {
capturedPanelProps = props
return (
<div data-testid="workflow-panel">
<div data-testid="panel-left">{props.components?.left}</div>
<div data-testid="panel-right">{props.components?.right}</div>
</div>
)
},
}))
// Mock Record component
vi.mock('@/app/components/workflow/panel/record', () => ({
default: () => <div data-testid="record-panel">Record Panel</div>,
}))
// Mock TestRunPanel component
vi.mock('@/app/components/rag-pipeline/components/panel/test-run', () => ({
default: () => <div data-testid="test-run-panel">Test Run Panel</div>,
}))
// Mock InputFieldPanel component
vi.mock('./input-field', () => ({
default: () => <div data-testid="input-field-panel">Input Field Panel</div>,
}))
// Mock InputFieldEditorPanel component
const mockInputFieldEditorProps = vi.fn()
vi.mock('./input-field/editor', () => ({
default: (props: Record<string, unknown>) => {
mockInputFieldEditorProps(props)
return <div data-testid="input-field-editor-panel">Input Field Editor Panel</div>
},
}))
// Mock PreviewPanel component
vi.mock('./input-field/preview', () => ({
default: () => <div data-testid="preview-panel">Preview Panel</div>,
}))
// Mock GlobalVariablePanel component
vi.mock('@/app/components/workflow/panel/global-variable-panel', () => ({
default: () => <div data-testid="global-variable-panel">Global Variable Panel</div>,
}))
// ============================================================================
// Helper Functions
// ============================================================================
type SetupMockOptions = {
historyWorkflowData?: Record<string, unknown> | null
showDebugAndPreviewPanel?: boolean
showGlobalVariablePanel?: boolean
showInputFieldPanel?: boolean
showInputFieldPreviewPanel?: boolean
inputFieldEditPanelProps?: Record<string, unknown> | null
pipelineId?: string
}
const setupMocks = (options?: SetupMockOptions) => {
mockHistoryWorkflowData = options?.historyWorkflowData ?? null
mockShowDebugAndPreviewPanel = options?.showDebugAndPreviewPanel ?? false
mockShowGlobalVariablePanel = options?.showGlobalVariablePanel ?? false
mockShowInputFieldPanel = options?.showInputFieldPanel ?? false
mockShowInputFieldPreviewPanel = options?.showInputFieldPreviewPanel ?? false
mockInputFieldEditPanelProps = options?.inputFieldEditPanelProps ?? null
mockPipelineId = options?.pipelineId ?? 'test-pipeline-123'
capturedPanelProps = null
}
// ============================================================================
// RagPipelinePanel Component Tests
// ============================================================================
describe('RagPipelinePanel', () => {
beforeEach(() => {
vi.clearAllMocks()
setupMocks()
})
// -------------------------------------------------------------------------
// Rendering Tests
// -------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', async () => {
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
})
})
it('should render Panel component with correct structure', async () => {
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('panel-left')).toBeInTheDocument()
expect(screen.getByTestId('panel-right')).toBeInTheDocument()
})
})
it('should pass versionHistoryPanelProps to Panel', async () => {
// Arrange
setupMocks({ pipelineId: 'my-pipeline-456' })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.versionHistoryPanelProps).toBeDefined()
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
'/rag/pipelines/my-pipeline-456/workflows',
)
})
})
})
// -------------------------------------------------------------------------
// Memoization Tests - versionHistoryPanelProps
// -------------------------------------------------------------------------
describe('Memoization - versionHistoryPanelProps', () => {
it('should compute correct getVersionListUrl based on pipelineId', async () => {
// Arrange
setupMocks({ pipelineId: 'pipeline-abc' })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
'/rag/pipelines/pipeline-abc/workflows',
)
})
})
it('should compute correct deleteVersionUrl function', async () => {
// Arrange
setupMocks({ pipelineId: 'pipeline-xyz' })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
const deleteUrl = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-1')
expect(deleteUrl).toBe('/rag/pipelines/pipeline-xyz/workflows/version-1')
})
})
it('should compute correct updateVersionUrl function', async () => {
// Arrange
setupMocks({ pipelineId: 'pipeline-def' })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
const updateUrl = capturedPanelProps?.versionHistoryPanelProps?.updateVersionUrl?.('version-2')
expect(updateUrl).toBe('/rag/pipelines/pipeline-def/workflows/version-2')
})
})
it('should set latestVersionId to empty string', async () => {
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.versionHistoryPanelProps?.latestVersionId).toBe('')
})
})
})
// -------------------------------------------------------------------------
// Memoization Tests - panelProps
// -------------------------------------------------------------------------
describe('Memoization - panelProps', () => {
it('should pass components.left to Panel', async () => {
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.components?.left).toBeDefined()
})
})
it('should pass components.right to Panel', async () => {
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.components?.right).toBeDefined()
})
})
it('should pass versionHistoryPanelProps to panelProps', async () => {
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.versionHistoryPanelProps).toBeDefined()
})
})
})
// -------------------------------------------------------------------------
// Component Memoization Tests (React.memo)
// -------------------------------------------------------------------------
describe('Component Memoization', () => {
it('should be wrapped with React.memo', async () => {
// The component should not break when re-rendered
const { rerender } = render(<RagPipelinePanel />)
// Act - rerender without prop changes
rerender(<RagPipelinePanel />)
// Assert - component should still render correctly
await waitFor(() => {
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
})
})
})
})
// ============================================================================
// RagPipelinePanelOnRight Component Tests
// ============================================================================
describe('RagPipelinePanelOnRight', () => {
beforeEach(() => {
vi.clearAllMocks()
setupMocks()
})
// -------------------------------------------------------------------------
// Conditional Rendering - Record Panel
// -------------------------------------------------------------------------
describe('Record Panel Conditional Rendering', () => {
it('should render Record panel when historyWorkflowData exists', async () => {
// Arrange
setupMocks({ historyWorkflowData: { id: 'history-1' } })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
})
})
it('should not render Record panel when historyWorkflowData is null', async () => {
// Arrange
setupMocks({ historyWorkflowData: null })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
})
})
it('should not render Record panel when historyWorkflowData is undefined', async () => {
// Arrange
setupMocks({ historyWorkflowData: undefined })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
})
})
})
// -------------------------------------------------------------------------
// Conditional Rendering - TestRun Panel
// -------------------------------------------------------------------------
describe('TestRun Panel Conditional Rendering', () => {
it('should render TestRun panel when showDebugAndPreviewPanel is true', async () => {
// Arrange
setupMocks({ showDebugAndPreviewPanel: true })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
})
})
it('should not render TestRun panel when showDebugAndPreviewPanel is false', async () => {
// Arrange
setupMocks({ showDebugAndPreviewPanel: false })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('test-run-panel')).not.toBeInTheDocument()
})
})
})
// -------------------------------------------------------------------------
// Conditional Rendering - GlobalVariable Panel
// -------------------------------------------------------------------------
describe('GlobalVariable Panel Conditional Rendering', () => {
it('should render GlobalVariable panel when showGlobalVariablePanel is true', async () => {
// Arrange
setupMocks({ showGlobalVariablePanel: true })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
})
})
it('should not render GlobalVariable panel when showGlobalVariablePanel is false', async () => {
// Arrange
setupMocks({ showGlobalVariablePanel: false })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('global-variable-panel')).not.toBeInTheDocument()
})
})
})
// -------------------------------------------------------------------------
// Multiple Panels Rendering
// -------------------------------------------------------------------------
describe('Multiple Panels Rendering', () => {
it('should render all right panels when all conditions are true', async () => {
// Arrange
setupMocks({
historyWorkflowData: { id: 'history-1' },
showDebugAndPreviewPanel: true,
showGlobalVariablePanel: true,
})
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
})
})
it('should render no right panels when all conditions are false', async () => {
// Arrange
setupMocks({
historyWorkflowData: null,
showDebugAndPreviewPanel: false,
showGlobalVariablePanel: false,
})
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('test-run-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('global-variable-panel')).not.toBeInTheDocument()
})
})
it('should render only Record and TestRun panels', async () => {
// Arrange
setupMocks({
historyWorkflowData: { id: 'history-1' },
showDebugAndPreviewPanel: true,
showGlobalVariablePanel: false,
})
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
expect(screen.queryByTestId('global-variable-panel')).not.toBeInTheDocument()
})
})
})
})
// ============================================================================
// RagPipelinePanelOnLeft Component Tests
// ============================================================================
describe('RagPipelinePanelOnLeft', () => {
beforeEach(() => {
vi.clearAllMocks()
setupMocks()
})
// -------------------------------------------------------------------------
// Conditional Rendering - Preview Panel
// -------------------------------------------------------------------------
describe('Preview Panel Conditional Rendering', () => {
it('should render Preview panel when showInputFieldPreviewPanel is true', async () => {
// Arrange
setupMocks({ showInputFieldPreviewPanel: true })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
})
})
it('should not render Preview panel when showInputFieldPreviewPanel is false', async () => {
// Arrange
setupMocks({ showInputFieldPreviewPanel: false })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('preview-panel')).not.toBeInTheDocument()
})
})
})
// -------------------------------------------------------------------------
// Conditional Rendering - InputFieldEditor Panel
// -------------------------------------------------------------------------
describe('InputFieldEditor Panel Conditional Rendering', () => {
it('should render InputFieldEditor panel when inputFieldEditPanelProps is provided', async () => {
// Arrange
const editProps = {
onClose: vi.fn(),
onSubmit: vi.fn(),
initialData: { variable: 'test' },
}
setupMocks({ inputFieldEditPanelProps: editProps })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
})
})
it('should not render InputFieldEditor panel when inputFieldEditPanelProps is null', async () => {
// Arrange
setupMocks({ inputFieldEditPanelProps: null })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
})
})
it('should pass props to InputFieldEditor panel', async () => {
// Arrange
const editProps = {
onClose: vi.fn(),
onSubmit: vi.fn(),
initialData: { variable: 'test_var', label: 'Test Label' },
}
setupMocks({ inputFieldEditPanelProps: editProps })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(mockInputFieldEditorProps).toHaveBeenCalledWith(
expect.objectContaining({
onClose: editProps.onClose,
onSubmit: editProps.onSubmit,
initialData: editProps.initialData,
}),
)
})
})
})
// -------------------------------------------------------------------------
// Conditional Rendering - InputField Panel
// -------------------------------------------------------------------------
describe('InputField Panel Conditional Rendering', () => {
it('should render InputField panel when showInputFieldPanel is true', async () => {
// Arrange
setupMocks({ showInputFieldPanel: true })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
})
})
it('should not render InputField panel when showInputFieldPanel is false', async () => {
// Arrange
setupMocks({ showInputFieldPanel: false })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('input-field-panel')).not.toBeInTheDocument()
})
})
})
// -------------------------------------------------------------------------
// Multiple Panels Rendering
// -------------------------------------------------------------------------
describe('Multiple Left Panels Rendering', () => {
it('should render all left panels when all conditions are true', async () => {
// Arrange
setupMocks({
showInputFieldPreviewPanel: true,
inputFieldEditPanelProps: { onClose: vi.fn(), onSubmit: vi.fn() },
showInputFieldPanel: true,
})
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
})
})
it('should render no left panels when all conditions are false', async () => {
// Arrange
setupMocks({
showInputFieldPreviewPanel: false,
inputFieldEditPanelProps: null,
showInputFieldPanel: false,
})
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.queryByTestId('preview-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
expect(screen.queryByTestId('input-field-panel')).not.toBeInTheDocument()
})
})
it('should render only Preview and InputField panels', async () => {
// Arrange
setupMocks({
showInputFieldPreviewPanel: true,
inputFieldEditPanelProps: null,
showInputFieldPanel: true,
})
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
})
})
})
})
// ============================================================================
// Edge Cases Tests
// ============================================================================
describe('Edge Cases', () => {
beforeEach(() => {
vi.clearAllMocks()
setupMocks()
})
// -------------------------------------------------------------------------
// Empty/Undefined Values
// -------------------------------------------------------------------------
describe('Empty/Undefined Values', () => {
it('should handle empty pipelineId gracefully', async () => {
// Arrange
setupMocks({ pipelineId: '' })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
'/rag/pipelines//workflows',
)
})
})
it('should handle special characters in pipelineId', async () => {
// Arrange
setupMocks({ pipelineId: 'pipeline-with-special_chars.123' })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
'/rag/pipelines/pipeline-with-special_chars.123/workflows',
)
})
})
})
// -------------------------------------------------------------------------
// Props Spreading Tests
// -------------------------------------------------------------------------
describe('Props Spreading', () => {
it('should correctly spread inputFieldEditPanelProps to editor component', async () => {
// Arrange
const customProps = {
onClose: vi.fn(),
onSubmit: vi.fn(),
initialData: {
variable: 'custom_var',
label: 'Custom Label',
type: 'text',
},
extraProp: 'extra-value',
}
setupMocks({ inputFieldEditPanelProps: customProps })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(mockInputFieldEditorProps).toHaveBeenCalledWith(
expect.objectContaining({
extraProp: 'extra-value',
}),
)
})
})
})
// -------------------------------------------------------------------------
// State Combinations
// -------------------------------------------------------------------------
describe('State Combinations', () => {
it('should handle all panels visible simultaneously', async () => {
// Arrange
setupMocks({
historyWorkflowData: { id: 'h1' },
showDebugAndPreviewPanel: true,
showGlobalVariablePanel: true,
showInputFieldPreviewPanel: true,
inputFieldEditPanelProps: { onClose: vi.fn(), onSubmit: vi.fn() },
showInputFieldPanel: true,
})
// Act
render(<RagPipelinePanel />)
// Assert - All panels should be visible
await waitFor(() => {
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
})
})
})
})
// ============================================================================
// URL Generator Functions Tests
// ============================================================================
describe('URL Generator Functions', () => {
beforeEach(() => {
vi.clearAllMocks()
setupMocks()
})
it('should return consistent URLs for same versionId', async () => {
// Arrange
setupMocks({ pipelineId: 'stable-pipeline' })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
const deleteUrl1 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-x')
const deleteUrl2 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-x')
expect(deleteUrl1).toBe(deleteUrl2)
})
})
it('should return different URLs for different versionIds', async () => {
// Arrange
setupMocks({ pipelineId: 'stable-pipeline' })
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
const deleteUrl1 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-1')
const deleteUrl2 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-2')
expect(deleteUrl1).not.toBe(deleteUrl2)
expect(deleteUrl1).toBe('/rag/pipelines/stable-pipeline/workflows/version-1')
expect(deleteUrl2).toBe('/rag/pipelines/stable-pipeline/workflows/version-2')
})
})
})
// ============================================================================
// Type Safety Tests
// ============================================================================
describe('Type Safety', () => {
beforeEach(() => {
vi.clearAllMocks()
setupMocks()
})
it('should pass correct PanelProps structure', async () => {
// Act
render(<RagPipelinePanel />)
// Assert - Check structure matches PanelProps
await waitFor(() => {
expect(capturedPanelProps).toHaveProperty('components')
expect(capturedPanelProps).toHaveProperty('versionHistoryPanelProps')
expect(capturedPanelProps?.components).toHaveProperty('left')
expect(capturedPanelProps?.components).toHaveProperty('right')
})
})
it('should pass correct versionHistoryPanelProps structure', async () => {
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('getVersionListUrl')
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('deleteVersionUrl')
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('updateVersionUrl')
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('latestVersionId')
})
})
})
// ============================================================================
// Performance Tests
// ============================================================================
describe('Performance', () => {
beforeEach(() => {
vi.clearAllMocks()
setupMocks()
})
it('should handle multiple rerenders without issues', async () => {
// Arrange
const { rerender } = render(<RagPipelinePanel />)
// Act - Multiple rerenders
for (let i = 0; i < 10; i++)
rerender(<RagPipelinePanel />)
// Assert - Component should still work
await waitFor(() => {
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
})
})
})
// ============================================================================
// Integration Tests
// ============================================================================
describe('Integration Tests', () => {
beforeEach(() => {
vi.clearAllMocks()
setupMocks()
})
it('should pass correct components to Panel', async () => {
// Arrange
setupMocks({
historyWorkflowData: { id: 'h1' },
showInputFieldPanel: true,
})
// Act
render(<RagPipelinePanel />)
// Assert
await waitFor(() => {
expect(capturedPanelProps?.components?.left).toBeDefined()
expect(capturedPanelProps?.components?.right).toBeDefined()
// Check that the components are React elements
expect(React.isValidElement(capturedPanelProps?.components?.left)).toBe(true)
expect(React.isValidElement(capturedPanelProps?.components?.right)).toBe(true)
})
})
it('should correctly consume all store selectors', async () => {
// Arrange
setupMocks({
historyWorkflowData: { id: 'test-history' },
showDebugAndPreviewPanel: true,
showGlobalVariablePanel: true,
showInputFieldPanel: true,
showInputFieldPreviewPanel: true,
inputFieldEditPanelProps: { onClose: vi.fn(), onSubmit: vi.fn() },
pipelineId: 'integration-test-pipeline',
})
// Act
render(<RagPipelinePanel />)
// Assert - All store-dependent rendering should work
await waitFor(() => {
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
'/rag/pipelines/integration-test-pipeline/workflows',
)
})
})
})

View File

@ -49,6 +49,7 @@ const InputFieldEditorPanel = ({
</div>
<button
type="button"
data-testid="input-field-editor-close-btn"
className="absolute right-2.5 top-2.5 flex size-8 items-center justify-center"
onClick={onClose}
>

View File

@ -53,6 +53,7 @@ const FieldList = ({
{LabelRightContent}
</div>
<ActionButton
data-testid="field-list-add-btn"
onClick={() => handleOpenInputFieldEditor()}
disabled={readonly}
className={cn(readonly && 'cursor-not-allowed')}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,937 @@
import type { WorkflowRunningData } from '@/app/components/workflow/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { ChunkingMode } from '@/models/datasets'
import Header from './header'
// Import components after mocks
import TestRunPanel from './index'
// ============================================================================
// Mocks
// ============================================================================
// Mock workflow store
const mockIsPreparingDataSource = vi.fn(() => true)
const mockSetIsPreparingDataSource = vi.fn()
const mockWorkflowRunningData = vi.fn<() => WorkflowRunningData | undefined>(() => undefined)
const mockPipelineId = 'test-pipeline-id'
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
const state = {
isPreparingDataSource: mockIsPreparingDataSource(),
workflowRunningData: mockWorkflowRunningData(),
pipelineId: mockPipelineId,
}
return selector(state)
},
useWorkflowStore: () => ({
getState: () => ({
isPreparingDataSource: mockIsPreparingDataSource(),
setIsPreparingDataSource: mockSetIsPreparingDataSource,
}),
}),
}))
// Mock workflow interactions
const mockHandleCancelDebugAndPreviewPanel = vi.fn()
vi.mock('@/app/components/workflow/hooks', () => ({
useWorkflowInteractions: () => ({
handleCancelDebugAndPreviewPanel: mockHandleCancelDebugAndPreviewPanel,
}),
useWorkflowRun: () => ({
handleRun: vi.fn(),
}),
useToolIcon: () => 'mock-tool-icon',
}))
// Mock data source provider
vi.mock('@/app/components/datasets/documents/create-from-pipeline/data-source/store/provider', () => ({
default: ({ children }: { children: React.ReactNode }) => <div data-testid="data-source-provider">{children}</div>,
}))
// Mock Preparation component
vi.mock('./preparation', () => ({
default: () => <div data-testid="preparation-component">Preparation</div>,
}))
// Mock Result component (for TestRunPanel tests only)
vi.mock('./result', () => ({
default: () => <div data-testid="result-component">Result</div>,
}))
// Mock ResultPanel from workflow
vi.mock('@/app/components/workflow/run/result-panel', () => ({
default: (props: Record<string, unknown>) => (
<div data-testid="result-panel">
ResultPanel -
{' '}
{props.status as string}
</div>
),
}))
// Mock TracingPanel from workflow
vi.mock('@/app/components/workflow/run/tracing-panel', () => ({
default: (props: { list: unknown[] }) => (
<div data-testid="tracing-panel">
TracingPanel -
{' '}
{props.list?.length ?? 0}
{' '}
items
</div>
),
}))
// Mock Loading component
vi.mock('@/app/components/base/loading', () => ({
default: () => <div data-testid="loading">Loading...</div>,
}))
// Mock config
vi.mock('@/config', () => ({
RAG_PIPELINE_PREVIEW_CHUNK_NUM: 5,
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createMockWorkflowRunningData = (overrides: Partial<WorkflowRunningData> = {}): WorkflowRunningData => ({
result: {
status: WorkflowRunningStatus.Succeeded,
outputs: '{"test": "output"}',
outputs_truncated: false,
inputs: '{"test": "input"}',
inputs_truncated: false,
process_data_truncated: false,
error: undefined,
elapsed_time: 1000,
total_tokens: 100,
created_at: Date.now(),
created_by: 'Test User',
total_steps: 5,
exceptions_count: 0,
},
tracing: [],
...overrides,
})
const createMockGeneralOutputs = (chunkContents: string[] = ['chunk1', 'chunk2']) => ({
chunk_structure: ChunkingMode.text,
preview: chunkContents.map(content => ({ content })),
})
const createMockParentChildOutputs = (parentMode: 'paragraph' | 'full-doc' = 'paragraph') => ({
chunk_structure: ChunkingMode.parentChild,
parent_mode: parentMode,
preview: [
{ content: 'parent1', child_chunks: ['child1', 'child2'] },
{ content: 'parent2', child_chunks: ['child3', 'child4'] },
],
})
const createMockQAOutputs = () => ({
chunk_structure: ChunkingMode.qa,
qa_preview: [
{ question: 'Q1', answer: 'A1' },
{ question: 'Q2', answer: 'A2' },
],
})
// ============================================================================
// TestRunPanel Component Tests
// ============================================================================
describe('TestRunPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsPreparingDataSource.mockReturnValue(true)
mockWorkflowRunningData.mockReturnValue(undefined)
})
// Basic rendering tests
describe('Rendering', () => {
it('should render with correct container styles', () => {
const { container } = render(<TestRunPanel />)
const panelDiv = container.firstChild as HTMLElement
expect(panelDiv).toHaveClass('relative', 'flex', 'h-full', 'w-[480px]', 'flex-col')
})
it('should render Header component', () => {
render(<TestRunPanel />)
expect(screen.getByText('datasetPipeline.testRun.title')).toBeInTheDocument()
})
})
// Conditional rendering based on isPreparingDataSource
describe('Conditional Content Rendering', () => {
it('should render Preparation inside DataSourceProvider when isPreparingDataSource is true', () => {
mockIsPreparingDataSource.mockReturnValue(true)
render(<TestRunPanel />)
expect(screen.getByTestId('data-source-provider')).toBeInTheDocument()
expect(screen.getByTestId('preparation-component')).toBeInTheDocument()
expect(screen.queryByTestId('result-component')).not.toBeInTheDocument()
})
it('should render Result when isPreparingDataSource is false', () => {
mockIsPreparingDataSource.mockReturnValue(false)
render(<TestRunPanel />)
expect(screen.getByTestId('result-component')).toBeInTheDocument()
expect(screen.queryByTestId('data-source-provider')).not.toBeInTheDocument()
expect(screen.queryByTestId('preparation-component')).not.toBeInTheDocument()
})
})
})
// ============================================================================
// Header Component Tests
// ============================================================================
describe('Header', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsPreparingDataSource.mockReturnValue(true)
})
// Rendering tests
describe('Rendering', () => {
it('should render title with correct translation key', () => {
render(<Header />)
expect(screen.getByText('datasetPipeline.testRun.title')).toBeInTheDocument()
})
it('should render close button', () => {
render(<Header />)
const closeButton = screen.getByRole('button')
expect(closeButton).toBeInTheDocument()
})
it('should have correct layout classes', () => {
const { container } = render(<Header />)
const headerDiv = container.firstChild as HTMLElement
expect(headerDiv).toHaveClass('flex', 'items-center', 'gap-x-2', 'pl-4', 'pr-3', 'pt-4')
})
})
// Close button interactions
describe('Close Button Interaction', () => {
it('should call setIsPreparingDataSource(false) and handleCancelDebugAndPreviewPanel when clicked and isPreparingDataSource is true', () => {
mockIsPreparingDataSource.mockReturnValue(true)
render(<Header />)
const closeButton = screen.getByRole('button')
fireEvent.click(closeButton)
expect(mockSetIsPreparingDataSource).toHaveBeenCalledWith(false)
expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalledTimes(1)
})
it('should only call handleCancelDebugAndPreviewPanel when isPreparingDataSource is false', () => {
mockIsPreparingDataSource.mockReturnValue(false)
render(<Header />)
const closeButton = screen.getByRole('button')
fireEvent.click(closeButton)
expect(mockSetIsPreparingDataSource).not.toHaveBeenCalled()
expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalledTimes(1)
})
})
})
// ============================================================================
// Result Component Tests (Real Implementation)
// ============================================================================
// Unmock Result for these tests
vi.doUnmock('./result')
describe('Result', () => {
// Dynamically import Result to get real implementation
let Result: typeof import('./result').default
beforeAll(async () => {
const resultModule = await import('./result')
Result = resultModule.default
})
beforeEach(() => {
vi.clearAllMocks()
mockWorkflowRunningData.mockReturnValue(undefined)
})
// Rendering tests
describe('Rendering', () => {
it('should render with RESULT tab active by default', async () => {
render(<Result />)
await waitFor(() => {
const resultTab = screen.getByRole('button', { name: /runLog\.result/i })
expect(resultTab).toHaveClass('border-util-colors-blue-brand-blue-brand-600')
})
})
it('should render all three tabs', () => {
render(<Result />)
expect(screen.getByRole('button', { name: /runLog\.result/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /runLog\.detail/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /runLog\.tracing/i })).toBeInTheDocument()
})
})
// Tab switching tests
describe('Tab Switching', () => {
it('should switch to DETAIL tab when clicked', async () => {
mockWorkflowRunningData.mockReturnValue(createMockWorkflowRunningData())
render(<Result />)
const detailTab = screen.getByRole('button', { name: /runLog\.detail/i })
fireEvent.click(detailTab)
await waitFor(() => {
expect(screen.getByTestId('result-panel')).toBeInTheDocument()
})
})
it('should switch to TRACING tab when clicked', async () => {
mockWorkflowRunningData.mockReturnValue(createMockWorkflowRunningData({ tracing: [{ id: '1' }] as unknown as WorkflowRunningData['tracing'] }))
render(<Result />)
const tracingTab = screen.getByRole('button', { name: /runLog\.tracing/i })
fireEvent.click(tracingTab)
await waitFor(() => {
expect(screen.getByTestId('tracing-panel')).toBeInTheDocument()
})
})
})
// Loading states
describe('Loading States', () => {
it('should show loading in DETAIL tab when no result data', async () => {
mockWorkflowRunningData.mockReturnValue({
result: undefined as unknown as WorkflowRunningData['result'],
tracing: [],
})
render(<Result />)
const detailTab = screen.getByRole('button', { name: /runLog\.detail/i })
fireEvent.click(detailTab)
await waitFor(() => {
expect(screen.getByTestId('loading')).toBeInTheDocument()
})
})
it('should show loading in TRACING tab when no tracing data', async () => {
mockWorkflowRunningData.mockReturnValue(createMockWorkflowRunningData({ tracing: [] }))
render(<Result />)
const tracingTab = screen.getByRole('button', { name: /runLog\.tracing/i })
fireEvent.click(tracingTab)
await waitFor(() => {
expect(screen.getByTestId('loading')).toBeInTheDocument()
})
})
})
})
// ============================================================================
// ResultPreview Component Tests
// ============================================================================
// We need to import ResultPreview directly
vi.doUnmock('./result/result-preview')
describe('ResultPreview', () => {
let ResultPreview: typeof import('./result/result-preview').default
beforeAll(async () => {
const previewModule = await import('./result/result-preview')
ResultPreview = previewModule.default
})
const mockOnSwitchToDetail = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// Loading state
describe('Loading State', () => {
it('should show loading spinner when isRunning is true and no outputs', () => {
render(
<ResultPreview
isRunning={true}
outputs={undefined}
error={undefined}
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument()
})
it('should not show loading when outputs are available', () => {
render(
<ResultPreview
isRunning={true}
outputs={createMockGeneralOutputs()}
error={undefined}
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
expect(screen.queryByText('pipeline.result.resultPreview.loading')).not.toBeInTheDocument()
})
})
// Error state
describe('Error State', () => {
it('should show error message when not running and has error', () => {
render(
<ResultPreview
isRunning={false}
outputs={undefined}
error="Test error message"
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'pipeline.result.resultPreview.viewDetails' })).toBeInTheDocument()
})
it('should call onSwitchToDetail when View Details button is clicked', () => {
render(
<ResultPreview
isRunning={false}
outputs={undefined}
error="Test error message"
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
const viewDetailsButton = screen.getByRole('button', { name: 'pipeline.result.resultPreview.viewDetails' })
fireEvent.click(viewDetailsButton)
expect(mockOnSwitchToDetail).toHaveBeenCalledTimes(1)
})
it('should not show error when still running', () => {
render(
<ResultPreview
isRunning={true}
outputs={undefined}
error="Test error message"
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
expect(screen.queryByText('pipeline.result.resultPreview.error')).not.toBeInTheDocument()
})
})
// Success state with outputs
describe('Success State with Outputs', () => {
it('should render chunk content when outputs are available', () => {
render(
<ResultPreview
isRunning={false}
outputs={createMockGeneralOutputs(['test chunk content'])}
error={undefined}
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
// Check that chunk content is rendered (the real ChunkCardList renders the content)
expect(screen.getByText('test chunk content')).toBeInTheDocument()
})
it('should render multiple chunks when provided', () => {
render(
<ResultPreview
isRunning={false}
outputs={createMockGeneralOutputs(['chunk one', 'chunk two'])}
error={undefined}
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
expect(screen.getByText('chunk one')).toBeInTheDocument()
expect(screen.getByText('chunk two')).toBeInTheDocument()
})
it('should show footer tip', () => {
render(
<ResultPreview
isRunning={false}
outputs={createMockGeneralOutputs()}
error={undefined}
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
expect(screen.getByText(/pipeline\.result\.resultPreview\.footerTip/)).toBeInTheDocument()
})
})
// Edge cases
describe('Edge Cases', () => {
it('should handle empty outputs gracefully', () => {
render(
<ResultPreview
isRunning={false}
outputs={null}
error={undefined}
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
// Should not crash and should not show chunk card list
expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument()
})
it('should handle undefined outputs', () => {
render(
<ResultPreview
isRunning={false}
outputs={undefined}
error={undefined}
onSwitchToDetail={mockOnSwitchToDetail}
/>,
)
expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument()
})
})
})
// ============================================================================
// Tabs Component Tests
// ============================================================================
vi.doUnmock('./result/tabs')
describe('Tabs', () => {
let Tabs: typeof import('./result/tabs').default
beforeAll(async () => {
const tabsModule = await import('./result/tabs')
Tabs = tabsModule.default
})
const mockSwitchTab = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render all three tabs', () => {
render(
<Tabs
currentTab="RESULT"
workflowRunningData={createMockWorkflowRunningData()}
switchTab={mockSwitchTab}
/>,
)
expect(screen.getByRole('button', { name: /runLog\.result/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /runLog\.detail/i })).toBeInTheDocument()
expect(screen.getByRole('button', { name: /runLog\.tracing/i })).toBeInTheDocument()
})
})
// Active tab styling
describe('Active Tab Styling', () => {
it('should highlight RESULT tab when currentTab is RESULT', () => {
render(
<Tabs
currentTab="RESULT"
workflowRunningData={createMockWorkflowRunningData()}
switchTab={mockSwitchTab}
/>,
)
const resultTab = screen.getByRole('button', { name: /runLog\.result/i })
expect(resultTab).toHaveClass('border-util-colors-blue-brand-blue-brand-600')
})
it('should highlight DETAIL tab when currentTab is DETAIL', () => {
render(
<Tabs
currentTab="DETAIL"
workflowRunningData={createMockWorkflowRunningData()}
switchTab={mockSwitchTab}
/>,
)
const detailTab = screen.getByRole('button', { name: /runLog\.detail/i })
expect(detailTab).toHaveClass('border-util-colors-blue-brand-blue-brand-600')
})
})
// Tab click handling
describe('Tab Click Handling', () => {
it('should call switchTab with RESULT when RESULT tab is clicked', () => {
render(
<Tabs
currentTab="DETAIL"
workflowRunningData={createMockWorkflowRunningData()}
switchTab={mockSwitchTab}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /runLog\.result/i }))
expect(mockSwitchTab).toHaveBeenCalledWith('RESULT')
})
it('should call switchTab with DETAIL when DETAIL tab is clicked', () => {
render(
<Tabs
currentTab="RESULT"
workflowRunningData={createMockWorkflowRunningData()}
switchTab={mockSwitchTab}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /runLog\.detail/i }))
expect(mockSwitchTab).toHaveBeenCalledWith('DETAIL')
})
it('should call switchTab with TRACING when TRACING tab is clicked', () => {
render(
<Tabs
currentTab="RESULT"
workflowRunningData={createMockWorkflowRunningData()}
switchTab={mockSwitchTab}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /runLog\.tracing/i }))
expect(mockSwitchTab).toHaveBeenCalledWith('TRACING')
})
})
// Disabled state when no data
describe('Disabled State', () => {
it('should disable tabs when workflowRunningData is undefined', () => {
render(
<Tabs
currentTab="RESULT"
workflowRunningData={undefined}
switchTab={mockSwitchTab}
/>,
)
const resultTab = screen.getByRole('button', { name: /runLog\.result/i })
expect(resultTab).toBeDisabled()
})
})
})
// ============================================================================
// Tab Component Tests
// ============================================================================
vi.doUnmock('./result/tabs/tab')
describe('Tab', () => {
let Tab: typeof import('./result/tabs/tab').default
beforeAll(async () => {
const tabModule = await import('./result/tabs/tab')
Tab = tabModule.default
})
const mockOnClick = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// Rendering tests
describe('Rendering', () => {
it('should render tab with label', () => {
render(
<Tab
isActive={false}
label="Test Tab"
value="TEST"
workflowRunningData={createMockWorkflowRunningData()}
onClick={mockOnClick}
/>,
)
expect(screen.getByRole('button', { name: 'Test Tab' })).toBeInTheDocument()
})
})
// Active state styling
describe('Active State', () => {
it('should have active styles when isActive is true', () => {
render(
<Tab
isActive={true}
label="Active Tab"
value="TEST"
workflowRunningData={createMockWorkflowRunningData()}
onClick={mockOnClick}
/>,
)
const tab = screen.getByRole('button')
expect(tab).toHaveClass('border-util-colors-blue-brand-blue-brand-600', 'text-text-primary')
})
it('should have inactive styles when isActive is false', () => {
render(
<Tab
isActive={false}
label="Inactive Tab"
value="TEST"
workflowRunningData={createMockWorkflowRunningData()}
onClick={mockOnClick}
/>,
)
const tab = screen.getByRole('button')
expect(tab).toHaveClass('border-transparent', 'text-text-tertiary')
})
})
// Click handling
describe('Click Handling', () => {
it('should call onClick with value when clicked', () => {
render(
<Tab
isActive={false}
label="Test Tab"
value="MY_VALUE"
workflowRunningData={createMockWorkflowRunningData()}
onClick={mockOnClick}
/>,
)
fireEvent.click(screen.getByRole('button'))
expect(mockOnClick).toHaveBeenCalledWith('MY_VALUE')
})
it('should not call onClick when disabled (no workflowRunningData)', () => {
render(
<Tab
isActive={false}
label="Test Tab"
value="MY_VALUE"
workflowRunningData={undefined}
onClick={mockOnClick}
/>,
)
const tab = screen.getByRole('button')
fireEvent.click(tab)
// The click handler is still called, but button is disabled
expect(tab).toBeDisabled()
})
})
// Disabled state
describe('Disabled State', () => {
it('should be disabled when workflowRunningData is undefined', () => {
render(
<Tab
isActive={false}
label="Test Tab"
value="TEST"
workflowRunningData={undefined}
onClick={mockOnClick}
/>,
)
const tab = screen.getByRole('button')
expect(tab).toBeDisabled()
expect(tab).toHaveClass('opacity-30')
})
it('should not be disabled when workflowRunningData is provided', () => {
render(
<Tab
isActive={false}
label="Test Tab"
value="TEST"
workflowRunningData={createMockWorkflowRunningData()}
onClick={mockOnClick}
/>,
)
const tab = screen.getByRole('button')
expect(tab).not.toBeDisabled()
})
})
})
// ============================================================================
// formatPreviewChunks Utility Tests
// ============================================================================
describe('formatPreviewChunks', () => {
let formatPreviewChunks: typeof import('./result/result-preview/utils').formatPreviewChunks
beforeAll(async () => {
const utilsModule = await import('./result/result-preview/utils')
formatPreviewChunks = utilsModule.formatPreviewChunks
})
// Edge cases
describe('Edge Cases', () => {
it('should return undefined for null outputs', () => {
expect(formatPreviewChunks(null)).toBeUndefined()
})
it('should return undefined for undefined outputs', () => {
expect(formatPreviewChunks(undefined)).toBeUndefined()
})
it('should return undefined for unknown chunk structure', () => {
const outputs = {
chunk_structure: 'unknown_mode',
preview: [],
}
expect(formatPreviewChunks(outputs)).toBeUndefined()
})
})
// General (text) chunks
describe('General Chunks (ChunkingMode.text)', () => {
it('should format general chunks correctly', () => {
const outputs = createMockGeneralOutputs(['content1', 'content2', 'content3'])
const result = formatPreviewChunks(outputs)
expect(result).toEqual(['content1', 'content2', 'content3'])
})
it('should limit to RAG_PIPELINE_PREVIEW_CHUNK_NUM chunks', () => {
const manyChunks = Array.from({ length: 10 }, (_, i) => `chunk${i}`)
const outputs = createMockGeneralOutputs(manyChunks)
const result = formatPreviewChunks(outputs) as string[]
// RAG_PIPELINE_PREVIEW_CHUNK_NUM is mocked to 5
expect(result).toHaveLength(5)
expect(result).toEqual(['chunk0', 'chunk1', 'chunk2', 'chunk3', 'chunk4'])
})
it('should handle empty preview array', () => {
const outputs = createMockGeneralOutputs([])
const result = formatPreviewChunks(outputs)
expect(result).toEqual([])
})
})
// Parent-child chunks
describe('Parent-Child Chunks (ChunkingMode.parentChild)', () => {
it('should format paragraph mode parent-child chunks correctly', () => {
const outputs = createMockParentChildOutputs('paragraph')
const result = formatPreviewChunks(outputs)
expect(result).toEqual({
parent_child_chunks: [
{ parent_content: 'parent1', child_contents: ['child1', 'child2'], parent_mode: 'paragraph' },
{ parent_content: 'parent2', child_contents: ['child3', 'child4'], parent_mode: 'paragraph' },
],
parent_mode: 'paragraph',
})
})
it('should format full-doc mode parent-child chunks and limit child chunks', () => {
const outputs = {
chunk_structure: ChunkingMode.parentChild,
parent_mode: 'full-doc' as const,
preview: [
{
content: 'full-doc-parent',
child_chunks: Array.from({ length: 10 }, (_, i) => `child${i}`),
},
],
}
const result = formatPreviewChunks(outputs)
expect(result).toEqual({
parent_child_chunks: [
{
parent_content: 'full-doc-parent',
child_contents: ['child0', 'child1', 'child2', 'child3', 'child4'], // Limited to 5
parent_mode: 'full-doc',
},
],
parent_mode: 'full-doc',
})
})
})
// QA chunks
describe('QA Chunks (ChunkingMode.qa)', () => {
it('should format QA chunks correctly', () => {
const outputs = createMockQAOutputs()
const result = formatPreviewChunks(outputs)
expect(result).toEqual({
qa_chunks: [
{ question: 'Q1', answer: 'A1' },
{ question: 'Q2', answer: 'A2' },
],
})
})
it('should limit QA chunks to RAG_PIPELINE_PREVIEW_CHUNK_NUM', () => {
const outputs = {
chunk_structure: ChunkingMode.qa,
qa_preview: Array.from({ length: 10 }, (_, i) => ({
question: `Q${i}`,
answer: `A${i}`,
})),
}
const result = formatPreviewChunks(outputs) as { qa_chunks: Array<{ question: string, answer: string }> }
expect(result.qa_chunks).toHaveLength(5)
})
})
})
// ============================================================================
// Types Tests
// ============================================================================
describe('Types', () => {
describe('TestRunStep Enum', () => {
it('should have correct enum values', async () => {
const { TestRunStep } = await import('./types')
expect(TestRunStep.dataSource).toBe('dataSource')
expect(TestRunStep.documentProcessing).toBe('documentProcessing')
})
})
})

View File

@ -0,0 +1,549 @@
import { fireEvent, render, screen } from '@testing-library/react'
import Actions from './index'
// ============================================================================
// Actions Component Tests
// ============================================================================
describe('Actions', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// -------------------------------------------------------------------------
// Rendering Tests
// -------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions handleNextStep={handleNextStep} />)
// Assert
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should render button with translated text', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions handleNextStep={handleNextStep} />)
// Assert - Translation mock returns key with namespace prefix
expect(screen.getByText('datasetCreation.stepOne.button')).toBeInTheDocument()
})
it('should render with correct container structure', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { container } = render(<Actions handleNextStep={handleNextStep} />)
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper.className).toContain('flex')
expect(wrapper.className).toContain('justify-end')
expect(wrapper.className).toContain('p-4')
expect(wrapper.className).toContain('pt-2')
})
it('should render span with px-0.5 class around text', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { container } = render(<Actions handleNextStep={handleNextStep} />)
// Assert
const span = container.querySelector('span')
expect(span).toBeInTheDocument()
expect(span?.className).toContain('px-0.5')
})
})
// -------------------------------------------------------------------------
// Props Variations Tests
// -------------------------------------------------------------------------
describe('Props Variations', () => {
it('should pass disabled=true to button when disabled prop is true', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
// Assert
expect(screen.getByRole('button')).toBeDisabled()
})
it('should pass disabled=false to button when disabled prop is false', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions disabled={false} handleNextStep={handleNextStep} />)
// Assert
expect(screen.getByRole('button')).not.toBeDisabled()
})
it('should not disable button when disabled prop is undefined', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions handleNextStep={handleNextStep} />)
// Assert
expect(screen.getByRole('button')).not.toBeDisabled()
})
it('should handle disabled switching from true to false', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { rerender } = render(
<Actions disabled={true} handleNextStep={handleNextStep} />,
)
// Assert - Initially disabled
expect(screen.getByRole('button')).toBeDisabled()
// Act - Rerender with disabled=false
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
// Assert - Now enabled
expect(screen.getByRole('button')).not.toBeDisabled()
})
it('should handle disabled switching from false to true', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { rerender } = render(
<Actions disabled={false} handleNextStep={handleNextStep} />,
)
// Assert - Initially enabled
expect(screen.getByRole('button')).not.toBeDisabled()
// Act - Rerender with disabled=true
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
// Assert - Now disabled
expect(screen.getByRole('button')).toBeDisabled()
})
it('should handle undefined disabled becoming true', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { rerender } = render(
<Actions handleNextStep={handleNextStep} />,
)
// Assert - Initially not disabled (undefined)
expect(screen.getByRole('button')).not.toBeDisabled()
// Act - Rerender with disabled=true
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
// Assert - Now disabled
expect(screen.getByRole('button')).toBeDisabled()
})
})
// -------------------------------------------------------------------------
// User Interaction Tests
// -------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call handleNextStep when button is clicked', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions handleNextStep={handleNextStep} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(handleNextStep).toHaveBeenCalledTimes(1)
})
it('should call handleNextStep exactly once per click', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions handleNextStep={handleNextStep} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(handleNextStep).toHaveBeenCalled()
expect(handleNextStep.mock.calls).toHaveLength(1)
})
it('should call handleNextStep multiple times on multiple clicks', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions handleNextStep={handleNextStep} />)
const button = screen.getByRole('button')
fireEvent.click(button)
fireEvent.click(button)
fireEvent.click(button)
// Assert
expect(handleNextStep).toHaveBeenCalledTimes(3)
})
it('should not call handleNextStep when button is disabled and clicked', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
fireEvent.click(screen.getByRole('button'))
// Assert - Disabled button should not trigger onClick
expect(handleNextStep).not.toHaveBeenCalled()
})
it('should handle rapid clicks when not disabled', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions handleNextStep={handleNextStep} />)
const button = screen.getByRole('button')
// Simulate rapid clicks
for (let i = 0; i < 10; i++)
fireEvent.click(button)
// Assert
expect(handleNextStep).toHaveBeenCalledTimes(10)
})
})
// -------------------------------------------------------------------------
// Callback Stability Tests
// -------------------------------------------------------------------------
describe('Callback Stability', () => {
it('should use the new handleNextStep when prop changes', () => {
// Arrange
const handleNextStep1 = vi.fn()
const handleNextStep2 = vi.fn()
// Act
const { rerender } = render(
<Actions handleNextStep={handleNextStep1} />,
)
fireEvent.click(screen.getByRole('button'))
rerender(<Actions handleNextStep={handleNextStep2} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(handleNextStep1).toHaveBeenCalledTimes(1)
expect(handleNextStep2).toHaveBeenCalledTimes(1)
})
it('should maintain functionality after rerender with same props', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { rerender } = render(
<Actions handleNextStep={handleNextStep} />,
)
fireEvent.click(screen.getByRole('button'))
rerender(<Actions handleNextStep={handleNextStep} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(handleNextStep).toHaveBeenCalledTimes(2)
})
it('should work correctly when handleNextStep changes multiple times', () => {
// Arrange
const handleNextStep1 = vi.fn()
const handleNextStep2 = vi.fn()
const handleNextStep3 = vi.fn()
// Act
const { rerender } = render(
<Actions handleNextStep={handleNextStep1} />,
)
fireEvent.click(screen.getByRole('button'))
rerender(<Actions handleNextStep={handleNextStep2} />)
fireEvent.click(screen.getByRole('button'))
rerender(<Actions handleNextStep={handleNextStep3} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(handleNextStep1).toHaveBeenCalledTimes(1)
expect(handleNextStep2).toHaveBeenCalledTimes(1)
expect(handleNextStep3).toHaveBeenCalledTimes(1)
})
})
// -------------------------------------------------------------------------
// Memoization Tests
// -------------------------------------------------------------------------
describe('Memoization', () => {
it('should be wrapped with React.memo', () => {
// Arrange
const handleNextStep = vi.fn()
// Act - Verify component is memoized by checking display name pattern
const { rerender } = render(
<Actions handleNextStep={handleNextStep} />,
)
// Rerender with same props should work without issues
rerender(<Actions handleNextStep={handleNextStep} />)
// Assert - Component should render correctly after rerender
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should not break when props remain the same across rerenders', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { rerender } = render(
<Actions disabled={false} handleNextStep={handleNextStep} />,
)
// Multiple rerenders with same props
for (let i = 0; i < 5; i++) {
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
}
// Assert - Should still function correctly
fireEvent.click(screen.getByRole('button'))
expect(handleNextStep).toHaveBeenCalledTimes(1)
})
it('should update correctly when only disabled prop changes', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { rerender } = render(
<Actions disabled={false} handleNextStep={handleNextStep} />,
)
// Assert - Initially not disabled
expect(screen.getByRole('button')).not.toBeDisabled()
// Act - Change only disabled prop
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
// Assert - Should reflect the new disabled state
expect(screen.getByRole('button')).toBeDisabled()
})
it('should update correctly when only handleNextStep prop changes', () => {
// Arrange
const handleNextStep1 = vi.fn()
const handleNextStep2 = vi.fn()
// Act
const { rerender } = render(
<Actions disabled={false} handleNextStep={handleNextStep1} />,
)
fireEvent.click(screen.getByRole('button'))
expect(handleNextStep1).toHaveBeenCalledTimes(1)
// Act - Change only handleNextStep prop
rerender(<Actions disabled={false} handleNextStep={handleNextStep2} />)
fireEvent.click(screen.getByRole('button'))
// Assert - New callback should be used
expect(handleNextStep1).toHaveBeenCalledTimes(1)
expect(handleNextStep2).toHaveBeenCalledTimes(1)
})
})
// -------------------------------------------------------------------------
// Edge Cases Tests
// -------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should call handleNextStep even if it has side effects', () => {
// Arrange
let sideEffectValue = 0
const handleNextStep = vi.fn(() => {
sideEffectValue = 42
})
// Act
render(<Actions handleNextStep={handleNextStep} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(handleNextStep).toHaveBeenCalledTimes(1)
expect(sideEffectValue).toBe(42)
})
it('should handle handleNextStep that returns a value', () => {
// Arrange
const handleNextStep = vi.fn(() => 'return value')
// Act
render(<Actions handleNextStep={handleNextStep} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(handleNextStep).toHaveBeenCalledTimes(1)
expect(handleNextStep).toHaveReturnedWith('return value')
})
it('should handle handleNextStep that is async', async () => {
// Arrange
const handleNextStep = vi.fn().mockResolvedValue(undefined)
// Act
render(<Actions handleNextStep={handleNextStep} />)
fireEvent.click(screen.getByRole('button'))
// Assert
expect(handleNextStep).toHaveBeenCalledTimes(1)
})
it('should render correctly with both disabled=true and handleNextStep', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
// Assert
const button = screen.getByRole('button')
expect(button).toBeDisabled()
})
it('should handle component unmount gracefully', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { unmount } = render(<Actions handleNextStep={handleNextStep} />)
// Assert - Unmount should not throw
expect(() => unmount()).not.toThrow()
})
it('should handle disabled as boolean-like falsy value', () => {
// Arrange
const handleNextStep = vi.fn()
// Act - Test with explicit false
render(<Actions disabled={false} handleNextStep={handleNextStep} />)
// Assert
expect(screen.getByRole('button')).not.toBeDisabled()
})
})
// -------------------------------------------------------------------------
// Accessibility Tests
// -------------------------------------------------------------------------
describe('Accessibility', () => {
it('should have button element that can receive focus', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions handleNextStep={handleNextStep} />)
const button = screen.getByRole('button')
// Assert - Button should be focusable (not disabled by default)
expect(button).not.toBeDisabled()
})
it('should indicate disabled state correctly', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
// Assert
expect(screen.getByRole('button')).toHaveAttribute('disabled')
})
})
// -------------------------------------------------------------------------
// Integration Tests
// -------------------------------------------------------------------------
describe('Integration', () => {
it('should work in a typical workflow: enable -> click -> disable', () => {
// Arrange
const handleNextStep = vi.fn()
// Act - Start enabled
const { rerender } = render(
<Actions disabled={false} handleNextStep={handleNextStep} />,
)
// Assert - Can click when enabled
expect(screen.getByRole('button')).not.toBeDisabled()
fireEvent.click(screen.getByRole('button'))
expect(handleNextStep).toHaveBeenCalledTimes(1)
// Act - Disable after click (simulating loading state)
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
// Assert - Cannot click when disabled
expect(screen.getByRole('button')).toBeDisabled()
fireEvent.click(screen.getByRole('button'))
expect(handleNextStep).toHaveBeenCalledTimes(1) // Still 1, not 2
// Act - Re-enable
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
// Assert - Can click again
expect(screen.getByRole('button')).not.toBeDisabled()
fireEvent.click(screen.getByRole('button'))
expect(handleNextStep).toHaveBeenCalledTimes(2)
})
it('should maintain consistent rendering across multiple state changes', () => {
// Arrange
const handleNextStep = vi.fn()
// Act
const { rerender } = render(
<Actions disabled={false} handleNextStep={handleNextStep} />,
)
// Toggle disabled state multiple times
const states = [true, false, true, false, true]
states.forEach((disabled) => {
rerender(<Actions disabled={disabled} handleNextStep={handleNextStep} />)
if (disabled)
expect(screen.getByRole('button')).toBeDisabled()
else
expect(screen.getByRole('button')).not.toBeDisabled()
})
// Assert - Button should still render correctly
expect(screen.getByRole('button')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepOne.button')).toBeInTheDocument()
})
})
})

File diff suppressed because it is too large Load Diff

View File

@ -85,7 +85,11 @@ const PublishAsKnowledgePipelineModal = ({
>
<div className="title-2xl-semi-bold relative flex items-center p-6 pb-3 pr-14 text-text-primary">
{t('common.publishAs', { ns: 'pipeline' })}
<div className="absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center" onClick={onCancel}>
<div
data-testid="publish-modal-close-btn"
className="absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center"
onClick={onCancel}
>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -49,6 +49,9 @@ const useConfigVision = (model: ModelConfig, {
variable_selector: ['sys', 'files'],
}
}
else if (!enabled) {
delete draft.configs
}
})
onChange(newPayload)
}, [isChatMode, onChange, payload])

View File

@ -19,6 +19,7 @@ import { useDocLink } from '@/context/i18n'
import useDocumentTitle from '@/hooks/use-document-title'
import { fetchInitValidateStatus, fetchSetupStatus, login, setup } from '@/service/common'
import { cn } from '@/utils/classnames'
import { encryptPassword as encodePassword } from '@/utils/encryption'
import Loading from '../components/base/loading'
const accountFormSchema = z.object({
@ -68,7 +69,7 @@ const InstallForm = () => {
url: '/login',
body: {
email: data.email,
password: data.password,
password: encodePassword(data.password),
},
})

View File

@ -1,4 +1,5 @@
import type { Viewport } from 'next'
import { Provider as JotaiProvider } from 'jotai'
import { ThemeProvider } from 'next-themes'
import { Instrument_Serif } from 'next/font/google'
import { NuqsAdapter } from 'nuqs/adapters/next/app'
@ -91,27 +92,29 @@ const LocaleLayout = async ({
{...datasetMap}
>
<ReactScanLoader />
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme={false}
>
<NuqsAdapter>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServer>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</I18nServer>
</TanstackQueryInitializer>
</SentryInitializer>
</BrowserInitializer>
</NuqsAdapter>
</ThemeProvider>
<JotaiProvider>
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
enableSystem
disableTransitionOnChange
enableColorScheme={false}
>
<NuqsAdapter>
<BrowserInitializer>
<SentryInitializer>
<TanstackQueryInitializer>
<I18nServer>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</I18nServer>
</TanstackQueryInitializer>
</SentryInitializer>
</BrowserInitializer>
</NuqsAdapter>
</ThemeProvider>
</JotaiProvider>
<RoutePrefixHandle />
</body>
</html>

View File

@ -1,266 +0,0 @@
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(33%, auto));
width: var(--max-width);
max-width: 100%;
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 34ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo,
.thirteen {
position: relative;
}
.thirteen {
display: flex;
justify-content: center;
align-items: center;
width: 75px;
height: 75px;
padding: 25px 10px;
margin-left: 16px;
transform: translateZ(0);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0px 2px 8px -1px #0000001a;
}
.thirteen::before,
.thirteen::after {
content: '';
position: absolute;
z-index: -1;
}
/* Conic Gradient Animation */
.thirteen::before {
animation: 6s rotate linear infinite;
width: 200%;
height: 200%;
background: var(--tile-border);
}
/* Inner Square */
.thirteen::after {
inset: 0;
padding: 1px;
border-radius: var(--border-radius);
background: linear-gradient(to bottom right,
rgba(var(--tile-start-rgb), 1),
rgba(var(--tile-end-rgb), 1));
background-clip: content-box;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.thirteen::before {
animation: none;
}
.card:hover span {
transform: none;
}
}
/* Mobile and Tablet */
@media (max-width: 1023px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5));
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%);
z-index: 1;
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo,
.thirteen img {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}

View File

@ -0,0 +1,158 @@
import type { MockedFunction } from 'vitest'
import type { SystemFeatures } from '@/types/feature'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useLocale } from '@/context/i18n'
import { useSendMail } from '@/service/use-common'
import { defaultSystemFeatures } from '@/types/feature'
import Form from './input-mail'
const mockSubmitMail = vi.fn()
const mockOnSuccess = vi.fn()
type SystemFeaturesOverrides = Partial<Omit<SystemFeatures, 'branding'>> & {
branding?: Partial<SystemFeatures['branding']>
}
const buildSystemFeatures = (overrides: SystemFeaturesOverrides = {}): SystemFeatures => ({
...defaultSystemFeatures,
...overrides,
branding: {
...defaultSystemFeatures.branding,
...overrides.branding,
},
})
vi.mock('next/link', () => ({
default: ({ children, href, className, target, rel }: { children: React.ReactNode, href: string, className?: string, target?: string, rel?: string }) => (
<a href={href} className={className} target={target} rel={rel}>
{children}
</a>
),
}))
vi.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: vi.fn(),
}))
vi.mock('@/context/i18n', () => ({
useLocale: vi.fn(),
}))
vi.mock('@/service/use-common', () => ({
useSendMail: vi.fn(),
}))
type UseSendMailResult = ReturnType<typeof useSendMail>
const mockUseGlobalPublicStore = useGlobalPublicStore as unknown as MockedFunction<typeof useGlobalPublicStore>
const mockUseLocale = useLocale as unknown as MockedFunction<typeof useLocale>
const mockUseSendMail = useSendMail as unknown as MockedFunction<typeof useSendMail>
const renderForm = ({
brandingEnabled = false,
isPending = false,
}: {
brandingEnabled?: boolean
isPending?: boolean
} = {}) => {
mockUseGlobalPublicStore.mockReturnValue({
systemFeatures: buildSystemFeatures({
branding: { enabled: brandingEnabled },
}),
})
mockUseLocale.mockReturnValue('en-US')
mockUseSendMail.mockReturnValue({
mutateAsync: mockSubmitMail,
isPending,
} as unknown as UseSendMailResult)
return render(<Form onSuccess={mockOnSuccess} />)
}
describe('InputMail Form', () => {
beforeEach(() => {
vi.clearAllMocks()
mockSubmitMail.mockResolvedValue({ result: 'success', data: 'token' })
})
// Rendering baseline UI elements.
describe('Rendering', () => {
it('should render email input and submit button', () => {
renderForm()
expect(screen.getByLabelText('login.email')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'login.signup.verifyMail' })).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'login.signup.signIn' })).toBeInTheDocument()
})
})
// Prop-driven branding content visibility.
describe('Props', () => {
it('should show terms links when branding is disabled', () => {
renderForm({ brandingEnabled: false })
expect(screen.getByRole('link', { name: 'login.tos' })).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'login.pp' })).toBeInTheDocument()
})
it('should hide terms links when branding is enabled', () => {
renderForm({ brandingEnabled: true })
expect(screen.queryByRole('link', { name: 'login.tos' })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: 'login.pp' })).not.toBeInTheDocument()
})
})
// Submission flow and mutation integration.
describe('User Interactions', () => {
it('should submit email and call onSuccess when mutation succeeds', async () => {
renderForm()
const input = screen.getByLabelText('login.email')
const button = screen.getByRole('button', { name: 'login.signup.verifyMail' })
fireEvent.change(input, { target: { value: 'test@example.com' } })
fireEvent.click(button)
expect(mockSubmitMail).toHaveBeenCalledWith({
email: 'test@example.com',
language: 'en-US',
})
await waitFor(() => {
expect(mockOnSuccess).toHaveBeenCalledWith('test@example.com', 'token')
})
})
})
// Validation and failure paths.
describe('Edge Cases', () => {
it('should block submission when email is invalid', () => {
const { container } = renderForm()
const form = container.querySelector('form')
const input = screen.getByLabelText('login.email')
fireEvent.change(input, { target: { value: 'invalid-email' } })
expect(form).not.toBeNull()
fireEvent.submit(form as HTMLFormElement)
expect(mockSubmitMail).not.toHaveBeenCalled()
expect(mockOnSuccess).not.toHaveBeenCalled()
})
it('should not call onSuccess when mutation does not succeed', async () => {
mockSubmitMail.mockResolvedValue({ result: 'failed', data: 'token' })
renderForm()
const input = screen.getByLabelText('login.email')
const button = screen.getByRole('button', { name: 'login.signup.verifyMail' })
fireEvent.change(input, { target: { value: 'test@example.com' } })
fireEvent.click(button)
await waitFor(() => {
expect(mockSubmitMail).toHaveBeenCalled()
})
expect(mockOnSuccess).not.toHaveBeenCalled()
})
})
})

View File

@ -1,6 +1,5 @@
'use client'
import type { MailSendResponse } from '@/service/use-common'
import { noop } from 'es-toolkit/function'
import Link from 'next/link'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -27,6 +26,9 @@ export default function Form({
const { mutateAsync: submitMail, isPending } = useSendMail()
const handleSubmit = useCallback(async () => {
if (isPending)
return
if (!email) {
Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) })
return
@ -41,10 +43,14 @@ export default function Form({
const res = await submitMail({ email, language: locale })
if ((res as MailSendResponse).result === 'success')
onSuccess(email, (res as MailSendResponse).data)
}, [email, locale, submitMail, t])
}, [email, locale, submitMail, t, isPending, onSuccess])
return (
<form onSubmit={noop}>
<form onSubmit={(e) => {
e.preventDefault()
handleSubmit()
}}
>
<div className="mb-3">
<label htmlFor="email" className="system-md-semibold my-2 text-text-secondary">
{t('email', { ns: 'login' })}
@ -65,7 +71,7 @@ export default function Form({
<Button
tabIndex={2}
variant="primary"
onClick={handleSubmit}
type="submit"
disabled={isPending || !email}
className="w-full"
>
@ -88,7 +94,7 @@ export default function Form({
<>
<div className="system-xs-regular mt-3 block w-full text-text-tertiary">
{t('tosDesc', { ns: 'login' })}
&nbsp;
&nbsp;
<Link
className="system-xs-medium text-text-secondary hover:underline"
target="_blank"
@ -97,7 +103,7 @@ export default function Form({
>
{t('tos', { ns: 'login' })}
</Link>
&nbsp;&&nbsp;
&nbsp;&&nbsp;
<Link
className="system-xs-medium text-text-secondary hover:underline"
target="_blank"

View File

@ -134,7 +134,7 @@ export const ProviderContextProvider = ({
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo } = useEducationStatus(!enableEducationPlan)
const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo, isFetchedAfterMount: isEducationDataFetchedAfterMount } = useEducationStatus(!enableEducationPlan)
const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false)
const [isAllowPublishAsCustomKnowledgePipelineTemplate, setIsAllowPublishAsCustomKnowledgePipelineTemplate] = useState(false)
@ -240,9 +240,9 @@ export const ProviderContextProvider = ({
datasetOperatorEnabled,
enableEducationPlan,
isEducationWorkspace,
isEducationAccount: educationAccountInfo?.is_student || false,
allowRefreshEducationVerify: educationAccountInfo?.allow_refresh || false,
educationAccountExpireAt: educationAccountInfo?.expire_at || null,
isEducationAccount: isEducationDataFetchedAfterMount ? (educationAccountInfo?.is_student ?? false) : false,
allowRefreshEducationVerify: isEducationDataFetchedAfterMount ? (educationAccountInfo?.allow_refresh ?? false) : false,
educationAccountExpireAt: isEducationDataFetchedAfterMount ? (educationAccountInfo?.expire_at ?? null) : null,
isLoadingEducationAccountInfo,
isFetchingEducationAccountInfo,
webappCopyrightEnabled,

View File

@ -29,11 +29,6 @@ export default {
const options = context.options[0] || {}
const mode = options.mode || 'any'
/**
* Check if this is a t() function call
* @param {import('estree').CallExpression} node
* @returns {boolean}
*/
function isTCall(node) {
// Direct t() call
if (node.callee.type === 'Identifier' && node.callee.name === 't')

View File

@ -19,26 +19,11 @@ export default {
create(context) {
const sourceCode = context.sourceCode
// Track all t() calls to fix
/** @type {Array<{ node: import('estree').CallExpression }>} */
const tCallsToFix = []
// Track variables with namespace prefix
/** @type {Map<string, { node: import('estree').VariableDeclarator, name: string, oldValue: string, newValue: string, ns: string }>} */
const variablesToFix = new Map()
// Track all namespaces used in the file (from legacy prefix detection)
/** @type {Set<string>} */
const namespacesUsed = new Set()
// Track variable values for template literal analysis
/** @type {Map<string, string>} */
const variableValues = new Map()
/**
* Analyze a template literal and extract namespace info
* @param {import('estree').TemplateLiteral} node
*/
function analyzeTemplateLiteral(node) {
const quasis = node.quasis
const expressions = node.expressions
@ -78,11 +63,6 @@ export default {
return { ns: null, canFix: false, fixedQuasis: null, variableToUpdate: null }
}
/**
* Build a fixed template literal string
* @param {string[]} quasis
* @param {import('estree').Expression[]} expressions
*/
function buildTemplateLiteral(quasis, expressions) {
let result = '`'
for (let i = 0; i < quasis.length; i++) {
@ -95,11 +75,6 @@ export default {
return result
}
/**
* Check if a t() call already has ns in its second argument
* @param {import('estree').CallExpression} node
* @returns {boolean}
*/
function hasNsArgument(node) {
if (node.arguments.length < 2)
return false

View File

@ -12,11 +12,6 @@ export default {
},
},
create(context) {
/**
* Check if a t() call has ns in its second argument
* @param {import('estree').CallExpression} node
* @returns {boolean}
*/
function hasNsOption(node) {
if (node.arguments.length < 2)
return false

View File

@ -1,5 +1,6 @@
// @ts-check
import antfu from '@antfu/eslint-config'
import pluginQuery from '@tanstack/eslint-plugin-query'
import sonar from 'eslint-plugin-sonarjs'
import storybook from 'eslint-plugin-storybook'
import tailwind from 'eslint-plugin-tailwindcss'
@ -13,6 +14,24 @@ export default antfu(
'react/no-forward-ref': 'off',
'react/no-use-context': 'off',
'react/prefer-namespace-import': 'error',
// React Compiler rules
// Set to warn for gradual adoption
'react-hooks/config': 'warn',
'react-hooks/error-boundaries': 'warn',
'react-hooks/component-hook-factories': 'warn',
'react-hooks/gating': 'warn',
'react-hooks/globals': 'warn',
'react-hooks/immutability': 'warn',
'react-hooks/preserve-manual-memoization': 'warn',
'react-hooks/purity': 'warn',
'react-hooks/refs': 'warn',
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/set-state-in-render': 'warn',
'react-hooks/static-components': 'warn',
'react-hooks/unsupported-syntax': 'warn',
'react-hooks/use-memo': 'warn',
'react-hooks/incompatible-library': 'warn',
},
},
nextjs: true,
@ -61,6 +80,7 @@ export default antfu(
},
},
storybook.configs['flat/recommended'],
...pluginQuery.configs['flat/recommended'],
// sonar
{
rules: {

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "اسم العرض",
"variableConfig.editModalTitle": "تعديل حقل إدخال",
"variableConfig.errorMsg.atLeastOneOption": "خيار واحد على الأقل مطلوب",
"variableConfig.errorMsg.jsonSchemaInvalid": "مخطط JSON ليس JSON صالحًا",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "يجب أن يكون نوع مخطط JSON \"object\"",
"variableConfig.errorMsg.labelNameRequired": "اسم التسمية مطلوب",
"variableConfig.errorMsg.optionRepeat": "يوجد خيارات مكررة",
"variableConfig.errorMsg.varNameCanBeRepeat": "اسم المتغير لا يمكن تكراره",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "أوقات الاتصال",
"modelProvider.card.buyQuota": "شراء حصة",
"modelProvider.card.callTimes": "أوقات الاتصال",
"modelProvider.card.modelAPI": "النماذج {{modelName}} تستخدم مفتاح واجهة برمجة التطبيقات.",
"modelProvider.card.modelNotSupported": "النماذج {{modelName}} غير مثبتة.",
"modelProvider.card.modelSupported": "النماذج {{modelName}} تستخدم هذا الحصة.",
"modelProvider.card.onTrial": "في التجربة",
"modelProvider.card.paid": "مدفوع",
"modelProvider.card.priorityUse": "أولوية الاستخدام",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "الرموز المجانية المتاحة المتبقية",
"modelProvider.rerankModel.key": "نموذج إعادة الترتيب",
"modelProvider.rerankModel.tip": "سيعيد نموذج إعادة الترتيب ترتيب قائمة المستندات المرشحة بناءً على المطابقة الدلالية مع استعلام المستخدم، مما يحسن نتائج الترتيب الدلالي",
"modelProvider.resetDate": "إعادة الضبط على {{date}}",
"modelProvider.searchModel": "نموذج البحث",
"modelProvider.selectModel": "اختر نموذجك",
"modelProvider.selector.emptySetting": "يرجى الانتقال إلى الإعدادات للتكوين",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Anzeigename",
"variableConfig.editModalTitle": "Eingabefeld bearbeiten",
"variableConfig.errorMsg.atLeastOneOption": "Mindestens eine Option ist erforderlich",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON-Schema ist kein gültiges JSON",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON-Schema muss den Typ \"object\" haben",
"variableConfig.errorMsg.labelNameRequired": "Labelname ist erforderlich",
"variableConfig.errorMsg.optionRepeat": "Hat Wiederholungsoptionen",
"variableConfig.errorMsg.varNameCanBeRepeat": "Variablenname kann nicht wiederholt werden",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Anrufzeiten",
"modelProvider.card.buyQuota": "Kontingent kaufen",
"modelProvider.card.callTimes": "Anrufzeiten",
"modelProvider.card.modelAPI": "{{modelName}}-Modelle verwenden den API-Schlüssel.",
"modelProvider.card.modelNotSupported": "{{modelName}}-Modelle sind nicht installiert.",
"modelProvider.card.modelSupported": "{{modelName}}-Modelle verwenden dieses Kontingent.",
"modelProvider.card.onTrial": "In Probe",
"modelProvider.card.paid": "Bezahlt",
"modelProvider.card.priorityUse": "Priorisierte Nutzung",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Verbleibende verfügbare kostenlose Token",
"modelProvider.rerankModel.key": "Rerank-Modell",
"modelProvider.rerankModel.tip": "Rerank-Modell wird die Kandidatendokumentenliste basierend auf der semantischen Übereinstimmung mit der Benutzeranfrage neu ordnen und die Ergebnisse der semantischen Rangordnung verbessern",
"modelProvider.resetDate": "Zurücksetzen bei {{date}}",
"modelProvider.searchModel": "Suchmodell",
"modelProvider.selectModel": "Wählen Sie Ihr Modell",
"modelProvider.selector.emptySetting": "Bitte gehen Sie zu den Einstellungen, um zu konfigurieren",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Display Name",
"variableConfig.editModalTitle": "Edit Input Field",
"variableConfig.errorMsg.atLeastOneOption": "At least one option is required",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema is not valid JSON",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema must have type \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Label name is required",
"variableConfig.errorMsg.optionRepeat": "Has repeat options",
"variableConfig.errorMsg.varNameCanBeRepeat": "Variable name can not be repeated",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Nombre para mostrar",
"variableConfig.editModalTitle": "Editar Campo de Entrada",
"variableConfig.errorMsg.atLeastOneOption": "Se requiere al menos una opción",
"variableConfig.errorMsg.jsonSchemaInvalid": "El esquema JSON no es un JSON válido",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "El esquema JSON debe tener el tipo \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Nombre de la etiqueta es requerido",
"variableConfig.errorMsg.optionRepeat": "Hay opciones repetidas",
"variableConfig.errorMsg.varNameCanBeRepeat": "El nombre de la variable no puede repetirse",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Tiempos de llamada",
"modelProvider.card.buyQuota": "Comprar Cuota",
"modelProvider.card.callTimes": "Tiempos de llamada",
"modelProvider.card.modelAPI": "Los modelos {{modelName}} están usando la clave de API.",
"modelProvider.card.modelNotSupported": "Los modelos {{modelName}} no están instalados.",
"modelProvider.card.modelSupported": "Los modelos {{modelName}} están utilizando esta cuota.",
"modelProvider.card.onTrial": "En prueba",
"modelProvider.card.paid": "Pagado",
"modelProvider.card.priorityUse": "Uso prioritario",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Tokens gratuitos restantes disponibles",
"modelProvider.rerankModel.key": "Modelo de Reordenar",
"modelProvider.rerankModel.tip": "El modelo de reordenar reordenará la lista de documentos candidatos basada en la coincidencia semántica con la consulta del usuario, mejorando los resultados de clasificación semántica",
"modelProvider.resetDate": "Reiniciar en {{date}}",
"modelProvider.searchModel": "Modelo de búsqueda",
"modelProvider.selectModel": "Selecciona tu modelo",
"modelProvider.selector.emptySetting": "Por favor ve a configuraciones para configurar",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "نام نمایشی",
"variableConfig.editModalTitle": "ویرایش فیلد ورودی",
"variableConfig.errorMsg.atLeastOneOption": "حداقل یک گزینه مورد نیاز است",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema یک JSON معتبر نیست",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "نوع JSON Schema باید \"object\" باشد",
"variableConfig.errorMsg.labelNameRequired": "نام برچسب الزامی است",
"variableConfig.errorMsg.optionRepeat": "دارای گزینه های تکرار",
"variableConfig.errorMsg.varNameCanBeRepeat": "نام متغیر را نمی توان تکرار کرد",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "تعداد فراخوانی",
"modelProvider.card.buyQuota": "خرید سهمیه",
"modelProvider.card.callTimes": "تعداد فراخوانی",
"modelProvider.card.modelAPI": "مدل‌های {{modelName}} در حال استفاده از کلید API هستند.",
"modelProvider.card.modelNotSupported": "مدل‌های {{modelName}} نصب نشده‌اند.",
"modelProvider.card.modelSupported": "مدل‌های {{modelName}} از این سهمیه استفاده می‌کنند.",
"modelProvider.card.onTrial": "در حال آزمایش",
"modelProvider.card.paid": "پرداخت شده",
"modelProvider.card.priorityUse": "استفاده با اولویت",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "توکن‌های رایگان باقی‌مانده در دسترس",
"modelProvider.rerankModel.key": "مدل رتبه‌بندی مجدد",
"modelProvider.rerankModel.tip": "مدل رتبه‌بندی مجدد، لیست اسناد کاندید را بر اساس تطابق معنایی با پرسش کاربر مرتب می‌کند و نتایج رتبه‌بندی معنایی را بهبود می‌بخشد",
"modelProvider.resetDate": "بازنشانی در {{date}}",
"modelProvider.searchModel": "جستجوی مدل",
"modelProvider.selectModel": "مدل خود را انتخاب کنید",
"modelProvider.selector.emptySetting": "لطفاً به تنظیمات بروید تا پیکربندی کنید",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Nom daffichage",
"variableConfig.editModalTitle": "Edit Input Field",
"variableConfig.errorMsg.atLeastOneOption": "At least one option is required",
"variableConfig.errorMsg.jsonSchemaInvalid": "Le schéma JSON nest pas un JSON valide",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Le schéma JSON doit avoir le type \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Label name is required",
"variableConfig.errorMsg.optionRepeat": "Has repeat options",
"variableConfig.errorMsg.varNameCanBeRepeat": "Variable name can not be repeated",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Temps d'appel",
"modelProvider.card.buyQuota": "Acheter Quota",
"modelProvider.card.callTimes": "Temps d'appel",
"modelProvider.card.modelAPI": "Les modèles {{modelName}} utilisent la clé API.",
"modelProvider.card.modelNotSupported": "Les modèles {{modelName}} ne sont pas installés.",
"modelProvider.card.modelSupported": "Les modèles {{modelName}} utilisent ce quota.",
"modelProvider.card.onTrial": "En Essai",
"modelProvider.card.paid": "Payé",
"modelProvider.card.priorityUse": "Utilisation prioritaire",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Tokens gratuits restants disponibles",
"modelProvider.rerankModel.key": "Modèle de Réorganisation",
"modelProvider.rerankModel.tip": "Le modèle de réorganisation réorganisera la liste des documents candidats en fonction de la correspondance sémantique avec la requête de l'utilisateur, améliorant ainsi les résultats du classement sémantique.",
"modelProvider.resetDate": "Réinitialiser sur {{date}}",
"modelProvider.searchModel": "Modèle de recherche",
"modelProvider.selectModel": "Sélectionnez votre modèle",
"modelProvider.selector.emptySetting": "Veuillez aller dans les paramètres pour configurer",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "प्रदर्शन नाम",
"variableConfig.editModalTitle": "इनपुट फ़ील्ड संपादित करें",
"variableConfig.errorMsg.atLeastOneOption": "कम से कम एक विकल्प आवश्यक है",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON स्कीमा मान्य JSON नहीं है",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON स्कीमा का प्रकार \"object\" होना चाहिए",
"variableConfig.errorMsg.labelNameRequired": "लेबल नाम आवश्यक है",
"variableConfig.errorMsg.optionRepeat": "विकल्प दोहराए गए हैं",
"variableConfig.errorMsg.varNameCanBeRepeat": "वेरिएबल नाम दोहराया नहीं जा सकता",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "कॉल समय",
"modelProvider.card.buyQuota": "कोटा खरीदें",
"modelProvider.card.callTimes": "कॉल समय",
"modelProvider.card.modelAPI": "{{modelName}} मॉडल एपीआई कुंजी का उपयोग कर रहे हैं।",
"modelProvider.card.modelNotSupported": "{{modelName}} मॉडल इंस्टॉल नहीं हैं।",
"modelProvider.card.modelSupported": "{{modelName}} मॉडल इस कोटा का उपयोग कर रहे हैं।",
"modelProvider.card.onTrial": "परीक्षण पर",
"modelProvider.card.paid": "भुगतान किया हुआ",
"modelProvider.card.priorityUse": "प्राथमिकता उपयोग",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "बचे हुए उपलब्ध मुफ्त टोकन",
"modelProvider.rerankModel.key": "रीरैंक मॉडल",
"modelProvider.rerankModel.tip": "रीरैंक मॉडल उपयोगकर्ता प्रश्न के साथ सांविधिक मेल के आधार पर उम्मीदवार दस्तावेज़ सूची को पुनः क्रमित करेगा, सांविधिक रैंकिंग के परिणामों में सुधार करेगा।",
"modelProvider.resetDate": "{{date}} पर रीसेट करें",
"modelProvider.searchModel": "खोज मॉडल",
"modelProvider.selectModel": "अपने मॉडल का चयन करें",
"modelProvider.selector.emptySetting": "कॉन्फ़िगर करने के लिए कृपया सेटिंग्स पर जाएं",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Nama Tampilan",
"variableConfig.editModalTitle": "Edit Bidang Input",
"variableConfig.errorMsg.atLeastOneOption": "Setidaknya satu opsi diperlukan",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema bukan JSON yang valid",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema harus bertipe \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Nama label diperlukan",
"variableConfig.errorMsg.optionRepeat": "Memiliki opsi pengulangan",
"variableConfig.errorMsg.varNameCanBeRepeat": "Nama variabel tidak dapat diulang",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Waktu panggilan",
"modelProvider.card.buyQuota": "Beli Kuota",
"modelProvider.card.callTimes": "Waktu panggilan",
"modelProvider.card.modelAPI": "Model {{modelName}} sedang menggunakan API Key.",
"modelProvider.card.modelNotSupported": "Model {{modelName}} tidak terpasang.",
"modelProvider.card.modelSupported": "Model {{modelName}} sedang menggunakan kuota ini.",
"modelProvider.card.onTrial": "Sedang Diadili",
"modelProvider.card.paid": "Dibayar",
"modelProvider.card.priorityUse": "Penggunaan prioritas",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Token gratis yang masih tersedia",
"modelProvider.rerankModel.key": "Peringkat ulang Model",
"modelProvider.rerankModel.tip": "Model rerank akan menyusun ulang daftar dokumen kandidat berdasarkan kecocokan semantik dengan kueri pengguna, meningkatkan hasil peringkat semantik",
"modelProvider.resetDate": "Atur ulang pada {{date}}",
"modelProvider.searchModel": "Model pencarian",
"modelProvider.selectModel": "Pilih model Anda",
"modelProvider.selector.emptySetting": "Silakan buka pengaturan untuk mengonfigurasi",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Nome visualizzato",
"variableConfig.editModalTitle": "Modifica Campo Input",
"variableConfig.errorMsg.atLeastOneOption": "È richiesta almeno un'opzione",
"variableConfig.errorMsg.jsonSchemaInvalid": "Lo schema JSON non è un JSON valido",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Lo schema JSON deve avere tipo \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Il nome dell'etichetta è richiesto",
"variableConfig.errorMsg.optionRepeat": "Ci sono opzioni ripetute",
"variableConfig.errorMsg.varNameCanBeRepeat": "Il nome della variabile non può essere ripetuto",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Numero di chiamate",
"modelProvider.card.buyQuota": "Acquista Quota",
"modelProvider.card.callTimes": "Numero di chiamate",
"modelProvider.card.modelAPI": "I modelli {{modelName}} stanno utilizzando la chiave API.",
"modelProvider.card.modelNotSupported": "I modelli {{modelName}} non sono installati.",
"modelProvider.card.modelSupported": "I modelli {{modelName}} stanno utilizzando questa quota.",
"modelProvider.card.onTrial": "In Prova",
"modelProvider.card.paid": "Pagato",
"modelProvider.card.priorityUse": "Uso prioritario",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Token gratuiti rimanenti disponibili",
"modelProvider.rerankModel.key": "Modello di Rerank",
"modelProvider.rerankModel.tip": "Il modello di rerank riordinerà la lista dei documenti candidati basandosi sulla corrispondenza semantica con la query dell'utente, migliorando i risultati del ranking semantico",
"modelProvider.resetDate": "Reimposta su {{date}}",
"modelProvider.searchModel": "Modello di ricerca",
"modelProvider.selectModel": "Seleziona il tuo modello",
"modelProvider.selector.emptySetting": "Per favore vai alle impostazioni per configurare",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "表示名",
"variableConfig.editModalTitle": "入力フィールドを編集",
"variableConfig.errorMsg.atLeastOneOption": "少なくとも 1 つのオプションが必要です",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSONスキーマが有効なJSONではありません",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSONスキーマのtypeは\"object\"である必要があります",
"variableConfig.errorMsg.labelNameRequired": "ラベル名は必須です",
"variableConfig.errorMsg.optionRepeat": "繰り返しオプションがあります",
"variableConfig.errorMsg.varNameCanBeRepeat": "変数名は繰り返すことができません",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "표시 이름",
"variableConfig.editModalTitle": "입력 필드 편집",
"variableConfig.errorMsg.atLeastOneOption": "적어도 하나의 옵션이 필요합니다",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON 스키마가 올바른 JSON이 아닙니다",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON 스키마의 type은 \"object\"이어야 합니다",
"variableConfig.errorMsg.labelNameRequired": "레이블명은 필수입니다",
"variableConfig.errorMsg.optionRepeat": "옵션이 중복되어 있습니다",
"variableConfig.errorMsg.varNameCanBeRepeat": "변수명은 중복될 수 없습니다",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "호출 횟수",
"modelProvider.card.buyQuota": "Buy Quota",
"modelProvider.card.callTimes": "호출 횟수",
"modelProvider.card.modelAPI": "{{modelName}} 모델이 API 키를 사용하고 있습니다.",
"modelProvider.card.modelNotSupported": "{{modelName}} 모델이 설치되지 않았습니다.",
"modelProvider.card.modelSupported": "{{modelName}} 모델이 이 할당량을 사용하고 있습니다.",
"modelProvider.card.onTrial": "트라이얼 중",
"modelProvider.card.paid": "유료",
"modelProvider.card.priorityUse": "우선 사용",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "남은 무료 토큰 사용 가능",
"modelProvider.rerankModel.key": "재랭크 모델",
"modelProvider.rerankModel.tip": "재랭크 모델은 사용자 쿼리와의 의미적 일치를 기반으로 후보 문서 목록을 재배열하여 의미적 순위를 향상시킵니다.",
"modelProvider.resetDate": "{{date}}에서 재설정",
"modelProvider.searchModel": "검색 모델",
"modelProvider.selectModel": "모델 선택",
"modelProvider.selector.emptySetting": "설정으로 이동하여 구성하세요",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Nazwa wyświetlana",
"variableConfig.editModalTitle": "Edytuj Pole Wejściowe",
"variableConfig.errorMsg.atLeastOneOption": "Wymagana jest co najmniej jedna opcja",
"variableConfig.errorMsg.jsonSchemaInvalid": "Schemat JSON nie jest prawidłowym JSON-em",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Schemat JSON musi mieć typ \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Wymagana nazwa etykiety",
"variableConfig.errorMsg.optionRepeat": "Powtarzają się opcje",
"variableConfig.errorMsg.varNameCanBeRepeat": "Nazwa zmiennej nie może się powtarzać",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Czasy wywołań",
"modelProvider.card.buyQuota": "Kup limit",
"modelProvider.card.callTimes": "Czasy wywołań",
"modelProvider.card.modelAPI": "Modele {{modelName}} używają klucza API.",
"modelProvider.card.modelNotSupported": "Modele {{modelName}} nie są zainstalowane.",
"modelProvider.card.modelSupported": "{{modelName}} modeli korzysta z tej kwoty.",
"modelProvider.card.onTrial": "Na próbę",
"modelProvider.card.paid": "Płatny",
"modelProvider.card.priorityUse": "Używanie z priorytetem",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Pozostałe dostępne darmowe tokeny",
"modelProvider.rerankModel.key": "Model ponownego rankingu",
"modelProvider.rerankModel.tip": "Model ponownego rankingu zmieni kolejność listy dokumentów kandydatów na podstawie semantycznego dopasowania z zapytaniem użytkownika, poprawiając wyniki rankingu semantycznego",
"modelProvider.resetDate": "Reset na {{date}}",
"modelProvider.searchModel": "Model wyszukiwania",
"modelProvider.selectModel": "Wybierz swój model",
"modelProvider.selector.emptySetting": "Przejdź do ustawień, aby skonfigurować",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Nome de exibição",
"variableConfig.editModalTitle": "Editar Campo de Entrada",
"variableConfig.errorMsg.atLeastOneOption": "Pelo menos uma opção é obrigatória",
"variableConfig.errorMsg.jsonSchemaInvalid": "O JSON Schema não é um JSON válido",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "O JSON Schema deve ter o tipo \"object\"",
"variableConfig.errorMsg.labelNameRequired": "O nome do rótulo é obrigatório",
"variableConfig.errorMsg.optionRepeat": "Tem opções repetidas",
"variableConfig.errorMsg.varNameCanBeRepeat": "O nome da variável não pode ser repetido",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Chamadas",
"modelProvider.card.buyQuota": "Comprar Quota",
"modelProvider.card.callTimes": "Chamadas",
"modelProvider.card.modelAPI": "Os modelos {{modelName}} estão usando a Chave de API.",
"modelProvider.card.modelNotSupported": "Modelos {{modelName}} não estão instalados.",
"modelProvider.card.modelSupported": "Modelos {{modelName}} estão usando esta cota.",
"modelProvider.card.onTrial": "Em Teste",
"modelProvider.card.paid": "Pago",
"modelProvider.card.priorityUse": "Uso prioritário",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Tokens gratuitos disponíveis restantes",
"modelProvider.rerankModel.key": "Modelo de Reordenação",
"modelProvider.rerankModel.tip": "O modelo de reordenaenação reorganizará a lista de documentos candidatos com base na correspondência semântica com a consulta do usuário, melhorando os resultados da classificação semântica",
"modelProvider.resetDate": "Redefinir em {{date}}",
"modelProvider.searchModel": "Modelo de pesquisa",
"modelProvider.selectModel": "Selecione seu modelo",
"modelProvider.selector.emptySetting": "Por favor, vá para configurações para configurar",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Nume afișat",
"variableConfig.editModalTitle": "Editați câmpul de intrare",
"variableConfig.errorMsg.atLeastOneOption": "Este necesară cel puțin o opțiune",
"variableConfig.errorMsg.jsonSchemaInvalid": "Schema JSON nu este un JSON valid",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Schema JSON trebuie să aibă tipul \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Numele etichetei este obligatoriu",
"variableConfig.errorMsg.optionRepeat": "Există opțiuni repetate",
"variableConfig.errorMsg.varNameCanBeRepeat": "Numele variabilei nu poate fi repetat",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Apeluri",
"modelProvider.card.buyQuota": "Cumpără cotă",
"modelProvider.card.callTimes": "Apeluri",
"modelProvider.card.modelAPI": "Modelele {{modelName}} folosesc cheia API.",
"modelProvider.card.modelNotSupported": "Modelele {{modelName}} nu sunt instalate.",
"modelProvider.card.modelSupported": "{{modelName}} modele utilizează această cotă.",
"modelProvider.card.onTrial": "În probă",
"modelProvider.card.paid": "Plătit",
"modelProvider.card.priorityUse": "Utilizare prioritară",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Jetoane gratuite disponibile rămase",
"modelProvider.rerankModel.key": "Model de reordonare",
"modelProvider.rerankModel.tip": "Modelul de reordonare va reordona lista de documente candidate pe baza potrivirii semantice cu interogarea utilizatorului, îmbunătățind rezultatele clasificării semantice",
"modelProvider.resetDate": "Resetați la {{date}}",
"modelProvider.searchModel": "Model de căutare",
"modelProvider.selectModel": "Selectați modelul dvs.",
"modelProvider.selector.emptySetting": "Vă rugăm să mergeți la setări pentru a configura",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Отображаемое имя",
"variableConfig.editModalTitle": "Редактировать поле ввода",
"variableConfig.errorMsg.atLeastOneOption": "Требуется хотя бы один вариант",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema не является корректным JSON",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema должна иметь тип \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Имя метки обязательно",
"variableConfig.errorMsg.optionRepeat": "Есть повторяющиеся варианты",
"variableConfig.errorMsg.varNameCanBeRepeat": "Имя переменной не может повторяться",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Количество вызовов",
"modelProvider.card.buyQuota": "Купить квоту",
"modelProvider.card.callTimes": "Количество вызовов",
"modelProvider.card.modelAPI": "{{modelName}} модели используют ключ API.",
"modelProvider.card.modelNotSupported": "Модели {{modelName}} не установлены.",
"modelProvider.card.modelSupported": "Эту квоту используют модели {{modelName}}.",
"modelProvider.card.onTrial": "Пробная версия",
"modelProvider.card.paid": "Платный",
"modelProvider.card.priorityUse": "Приоритетное использование",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Оставшиеся доступные бесплатные токены",
"modelProvider.rerankModel.key": "Модель повторного ранжирования",
"modelProvider.rerankModel.tip": "Модель повторного ранжирования изменит порядок списка документов-кандидатов на основе семантического соответствия запросу пользователя, улучшая результаты семантического ранжирования",
"modelProvider.resetDate": "Сброс на {{date}}",
"modelProvider.searchModel": "Поиск модели",
"modelProvider.selectModel": "Выберите свою модель",
"modelProvider.selector.emptySetting": "Пожалуйста, перейдите в настройки для настройки",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "Prikazno ime",
"variableConfig.editModalTitle": "Uredi vnosno polje",
"variableConfig.errorMsg.atLeastOneOption": "Potrebna je vsaj ena možnost",
"variableConfig.errorMsg.jsonSchemaInvalid": "Shema JSON ni veljaven JSON",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "Shema JSON mora imeti tip \"object\"",
"variableConfig.errorMsg.labelNameRequired": "Ime nalepke je obvezno",
"variableConfig.errorMsg.optionRepeat": "Ima možnosti ponavljanja",
"variableConfig.errorMsg.varNameCanBeRepeat": "Imena spremenljivke ni mogoče ponoviti",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "Število klicev",
"modelProvider.card.buyQuota": "Kupi kvoto",
"modelProvider.card.callTimes": "Časi klicev",
"modelProvider.card.modelAPI": "{{modelName}} modeli uporabljajo API ključ.",
"modelProvider.card.modelNotSupported": "{{modelName}} modeli niso nameščeni.",
"modelProvider.card.modelSupported": "{{modelName}} modeli uporabljajo to kvoto.",
"modelProvider.card.onTrial": "Na preizkusu",
"modelProvider.card.paid": "Plačano",
"modelProvider.card.priorityUse": "Prednostna uporaba",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "Preostali razpoložljivi brezplačni žetoni",
"modelProvider.rerankModel.key": "Model za prerazvrstitev",
"modelProvider.rerankModel.tip": "Model za prerazvrstitev bo prerazporedil seznam kandidatskih dokumentov na podlagi semantične ujemanja z uporabniško poizvedbo, s čimer se izboljšajo rezultati semantičnega razvrščanja.",
"modelProvider.resetDate": "Ponastavi na {{date}}",
"modelProvider.searchModel": "Model iskanja",
"modelProvider.selectModel": "Izberite svoj model",
"modelProvider.selector.emptySetting": "Prosimo, pojdite v nastavitve za konfiguracijo",

View File

@ -306,6 +306,8 @@
"variableConfig.displayName": "ชื่อที่แสดง",
"variableConfig.editModalTitle": "แก้ไขฟิลด์อินพุต",
"variableConfig.errorMsg.atLeastOneOption": "จําเป็นต้องมีอย่างน้อยหนึ่งตัวเลือก",
"variableConfig.errorMsg.jsonSchemaInvalid": "JSON Schema ไม่ใช่ JSON ที่ถูกต้อง",
"variableConfig.errorMsg.jsonSchemaMustBeObject": "JSON Schema ต้องมีชนิดเป็น \"object\"",
"variableConfig.errorMsg.labelNameRequired": "ต้องมีชื่อฉลาก",
"variableConfig.errorMsg.optionRepeat": "มีตัวเลือกการทําซ้ํา",
"variableConfig.errorMsg.varNameCanBeRepeat": "ไม่สามารถทําซ้ําชื่อตัวแปรได้",

View File

@ -339,6 +339,9 @@
"modelProvider.callTimes": "เวลาโทร",
"modelProvider.card.buyQuota": "ซื้อโควต้า",
"modelProvider.card.callTimes": "เวลาโทร",
"modelProvider.card.modelAPI": "{{modelName}} โมเดลกำลังใช้คีย์ API",
"modelProvider.card.modelNotSupported": "โมเดล {{modelName}} ยังไม่ได้ติดตั้ง",
"modelProvider.card.modelSupported": "โมเดล {{modelName}} กำลังใช้โควต้านี้อยู่",
"modelProvider.card.onTrial": "ทดลองใช้",
"modelProvider.card.paid": "จ่าย",
"modelProvider.card.priorityUse": "ลําดับความสําคัญในการใช้งาน",
@ -394,6 +397,7 @@
"modelProvider.quotaTip": "โทเค็นฟรีที่เหลืออยู่",
"modelProvider.rerankModel.key": "จัดอันดับโมเดลใหม่",
"modelProvider.rerankModel.tip": "โมเดล Rerank จะจัดลําดับรายการเอกสารผู้สมัครใหม่ตามการจับคู่ความหมายกับการสืบค้นของผู้ใช้ ซึ่งช่วยปรับปรุงผลลัพธ์ของการจัดอันดับความหมาย",
"modelProvider.resetDate": "รีเซ็ตเมื่อ {{date}}",
"modelProvider.searchModel": "ค้นหารุ่น",
"modelProvider.selectModel": "เลือกรุ่นของคุณ",
"modelProvider.selector.emptySetting": "โปรดไปที่การตั้งค่าเพื่อกําหนดค่า",

Some files were not shown because too many files have changed in this diff Show More