From 5067e4f255c7d7bad6b57df75225f1165568db7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=86=E8=90=8C=E9=97=B7=E6=B2=B9=E7=93=B6?= Date: Thu, 18 Dec 2025 17:11:52 +0800 Subject: [PATCH 01/17] fix 29184 (#29188) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- api/core/rag/splitter/fixed_text_splitter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/core/rag/splitter/fixed_text_splitter.py b/api/core/rag/splitter/fixed_text_splitter.py index e95c009292..b65cb14d8e 100644 --- a/api/core/rag/splitter/fixed_text_splitter.py +++ b/api/core/rag/splitter/fixed_text_splitter.py @@ -95,7 +95,8 @@ class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter) splits = re.split(r" +", text) else: splits = text.split(separator) - splits = [item + separator if i < len(splits) else item for i, item in enumerate(splits)] + if self._keep_separator: + splits = [s + separator for s in splits[:-1]] + splits[-1:] else: splits = list(text) if separator == "\n": @@ -104,7 +105,7 @@ class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter) splits = [s for s in splits if (s not in {"", "\n"})] _good_splits = [] _good_splits_lengths = [] # cache the lengths of the splits - _separator = separator if self._keep_separator else "" + _separator = "" if self._keep_separator else separator s_lens = self._length_function(splits) if separator != "": for s, s_len in zip(splits, s_lens): From 5638dcc7adb321f31dc4becfd40c979bd7223c99 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 18 Dec 2025 17:18:24 +0800 Subject: [PATCH 02/17] chore: tests for configuration (#29870) --- .../config-vision/index.spec.tsx | 227 ++++++++++++++++ .../config/agent-setting-button.spec.tsx | 100 +++++++ .../config/config-audio.spec.tsx | 123 +++++++++ .../config/config-document.spec.tsx | 119 ++++++++ .../app/configuration/config/index.spec.tsx | 254 ++++++++++++++++++ 5 files changed, 823 insertions(+) create mode 100644 web/app/components/app/configuration/config-vision/index.spec.tsx create mode 100644 web/app/components/app/configuration/config/agent-setting-button.spec.tsx create mode 100644 web/app/components/app/configuration/config/config-audio.spec.tsx create mode 100644 web/app/components/app/configuration/config/config-document.spec.tsx create mode 100644 web/app/components/app/configuration/config/index.spec.tsx diff --git a/web/app/components/app/configuration/config-vision/index.spec.tsx b/web/app/components/app/configuration/config-vision/index.spec.tsx new file mode 100644 index 0000000000..e22db7b24e --- /dev/null +++ b/web/app/components/app/configuration/config-vision/index.spec.tsx @@ -0,0 +1,227 @@ +import React from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import ConfigVision from './index' +import ParamConfig from './param-config' +import ParamConfigContent from './param-config-content' +import type { FeatureStoreState } from '@/app/components/base/features/store' +import type { FileUpload } from '@/app/components/base/features/types' +import { Resolution, TransferMethod } from '@/types/app' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' + +const mockUseContext = jest.fn() +jest.mock('use-context-selector', () => { + const actual = jest.requireActual('use-context-selector') + return { + ...actual, + useContext: (context: unknown) => mockUseContext(context), + } +}) + +const mockUseFeatures = jest.fn() +const mockUseFeaturesStore = jest.fn() +jest.mock('@/app/components/base/features/hooks', () => ({ + useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector), + useFeaturesStore: () => mockUseFeaturesStore(), +})) + +const defaultFile: FileUpload = { + enabled: false, + allowed_file_types: [], + allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url], + number_limits: 3, + image: { + enabled: false, + detail: Resolution.low, + number_limits: 3, + transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url], + }, +} + +let featureStoreState: FeatureStoreState +let setFeaturesMock: jest.Mock + +const setupFeatureStore = (fileOverrides: Partial = {}) => { + const mergedFile: FileUpload = { + ...defaultFile, + ...fileOverrides, + image: { + ...defaultFile.image, + ...fileOverrides.image, + }, + } + featureStoreState = { + features: { + file: mergedFile, + }, + setFeatures: jest.fn(), + showFeaturesModal: false, + setShowFeaturesModal: jest.fn(), + } + setFeaturesMock = featureStoreState.setFeatures as jest.Mock + mockUseFeaturesStore.mockReturnValue({ + getState: () => featureStoreState, + }) + mockUseFeatures.mockImplementation(selector => selector(featureStoreState)) +} + +const getLatestFileConfig = () => { + expect(setFeaturesMock).toHaveBeenCalled() + const latestFeatures = setFeaturesMock.mock.calls[setFeaturesMock.mock.calls.length - 1][0] as { file: FileUpload } + return latestFeatures.file +} + +beforeEach(() => { + jest.clearAllMocks() + mockUseContext.mockReturnValue({ + isShowVisionConfig: true, + isAllowVideoUpload: false, + }) + setupFeatureStore() +}) + +// ConfigVision handles toggling file upload types + visibility rules. +describe('ConfigVision', () => { + it('should not render when vision configuration is hidden', () => { + mockUseContext.mockReturnValue({ + isShowVisionConfig: false, + isAllowVideoUpload: false, + }) + + render() + + expect(screen.queryByText('appDebug.vision.name')).not.toBeInTheDocument() + }) + + it('should show the toggle and parameter controls when visible', () => { + render() + + expect(screen.getByText('appDebug.vision.name')).toBeInTheDocument() + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false') + }) + + it('should enable both image and video uploads when toggled on with video support', async () => { + const user = userEvent.setup() + mockUseContext.mockReturnValue({ + isShowVisionConfig: true, + isAllowVideoUpload: true, + }) + setupFeatureStore({ + allowed_file_types: [], + }) + + render() + await user.click(screen.getByRole('switch')) + + const updatedFile = getLatestFileConfig() + expect(updatedFile.allowed_file_types).toEqual([SupportUploadFileTypes.image, SupportUploadFileTypes.video]) + expect(updatedFile.image?.enabled).toBe(true) + expect(updatedFile.enabled).toBe(true) + }) + + it('should disable image and video uploads when toggled off and no other types remain', async () => { + const user = userEvent.setup() + mockUseContext.mockReturnValue({ + isShowVisionConfig: true, + isAllowVideoUpload: true, + }) + setupFeatureStore({ + allowed_file_types: [SupportUploadFileTypes.image, SupportUploadFileTypes.video], + enabled: true, + image: { + enabled: true, + }, + }) + + render() + await user.click(screen.getByRole('switch')) + + const updatedFile = getLatestFileConfig() + expect(updatedFile.allowed_file_types).toEqual([]) + expect(updatedFile.enabled).toBe(false) + expect(updatedFile.image?.enabled).toBe(false) + }) + + it('should keep file uploads enabled when other file types remain after disabling vision', async () => { + const user = userEvent.setup() + mockUseContext.mockReturnValue({ + isShowVisionConfig: true, + isAllowVideoUpload: false, + }) + setupFeatureStore({ + allowed_file_types: [SupportUploadFileTypes.image, SupportUploadFileTypes.document], + enabled: true, + image: { enabled: true }, + }) + + render() + await user.click(screen.getByRole('switch')) + + const updatedFile = getLatestFileConfig() + expect(updatedFile.allowed_file_types).toEqual([SupportUploadFileTypes.document]) + expect(updatedFile.enabled).toBe(true) + expect(updatedFile.image?.enabled).toBe(false) + }) +}) + +// ParamConfig exposes ParamConfigContent via an inline trigger. +describe('ParamConfig', () => { + it('should toggle parameter panel when clicking the settings button', async () => { + setupFeatureStore() + const user = userEvent.setup() + + render() + + expect(screen.queryByText('appDebug.vision.visionSettings.title')).not.toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'appDebug.voice.settings' })) + + expect(await screen.findByText('appDebug.vision.visionSettings.title')).toBeInTheDocument() + }) +}) + +// ParamConfigContent manages resolution, upload source, and count limits. +describe('ParamConfigContent', () => { + it('should set resolution to high when the corresponding option is selected', async () => { + const user = userEvent.setup() + setupFeatureStore({ + image: { detail: Resolution.low }, + }) + + render() + + await user.click(screen.getByText('appDebug.vision.visionSettings.high')) + + const updatedFile = getLatestFileConfig() + expect(updatedFile.image?.detail).toBe(Resolution.high) + }) + + it('should switch upload method to local only', async () => { + const user = userEvent.setup() + setupFeatureStore({ + allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url], + }) + + render() + + await user.click(screen.getByText('appDebug.vision.visionSettings.localUpload')) + + const updatedFile = getLatestFileConfig() + expect(updatedFile.allowed_file_upload_methods).toEqual([TransferMethod.local_file]) + expect(updatedFile.image?.transfer_methods).toEqual([TransferMethod.local_file]) + }) + + it('should update upload limit value when input changes', async () => { + setupFeatureStore({ + number_limits: 2, + }) + + render() + const input = screen.getByRole('spinbutton') as HTMLInputElement + fireEvent.change(input, { target: { value: '4' } }) + + const updatedFile = getLatestFileConfig() + expect(updatedFile.number_limits).toBe(4) + expect(updatedFile.image?.number_limits).toBe(4) + }) +}) diff --git a/web/app/components/app/configuration/config/agent-setting-button.spec.tsx b/web/app/components/app/configuration/config/agent-setting-button.spec.tsx new file mode 100644 index 0000000000..db70865e51 --- /dev/null +++ b/web/app/components/app/configuration/config/agent-setting-button.spec.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import AgentSettingButton from './agent-setting-button' +import type { AgentConfig } from '@/models/debug' +import { AgentStrategy } from '@/types/app' + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +let latestAgentSettingProps: any +jest.mock('./agent/agent-setting', () => ({ + __esModule: true, + default: (props: any) => { + latestAgentSettingProps = props + return ( +
+ + +
+ ) + }, +})) + +const createAgentConfig = (overrides: Partial = {}): AgentConfig => ({ + enabled: true, + strategy: AgentStrategy.react, + max_iteration: 3, + tools: [], + ...overrides, +}) + +const setup = (overrides: Partial> = {}) => { + const props: React.ComponentProps = { + isFunctionCall: false, + isChatModel: true, + onAgentSettingChange: jest.fn(), + agentConfig: createAgentConfig(), + ...overrides, + } + + const user = userEvent.setup() + render() + return { props, user } +} + +beforeEach(() => { + jest.clearAllMocks() + latestAgentSettingProps = undefined +}) + +describe('AgentSettingButton', () => { + it('should render button label from translation key', () => { + setup() + + expect(screen.getByRole('button', { name: 'appDebug.agent.setting.name' })).toBeInTheDocument() + }) + + it('should open AgentSetting with the provided configuration when clicked', async () => { + const { user, props } = setup({ isFunctionCall: true, isChatModel: false }) + + await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' })) + + expect(screen.getByTestId('agent-setting')).toBeInTheDocument() + expect(latestAgentSettingProps.isFunctionCall).toBe(true) + expect(latestAgentSettingProps.isChatModel).toBe(false) + expect(latestAgentSettingProps.payload).toEqual(props.agentConfig) + }) + + it('should call onAgentSettingChange and close when AgentSetting saves', async () => { + const { user, props } = setup() + + await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' })) + await user.click(screen.getByText('save-agent')) + + expect(props.onAgentSettingChange).toHaveBeenCalledTimes(1) + expect(props.onAgentSettingChange).toHaveBeenCalledWith({ + ...props.agentConfig, + max_iteration: 9, + }) + expect(screen.queryByTestId('agent-setting')).not.toBeInTheDocument() + }) + + it('should close AgentSetting without saving when cancel is triggered', async () => { + const { user, props } = setup() + + await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' })) + await user.click(screen.getByText('cancel-agent')) + + expect(props.onAgentSettingChange).not.toHaveBeenCalled() + expect(screen.queryByTestId('agent-setting')).not.toBeInTheDocument() + }) +}) diff --git a/web/app/components/app/configuration/config/config-audio.spec.tsx b/web/app/components/app/configuration/config/config-audio.spec.tsx new file mode 100644 index 0000000000..94eeb87c99 --- /dev/null +++ b/web/app/components/app/configuration/config/config-audio.spec.tsx @@ -0,0 +1,123 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import ConfigAudio from './config-audio' +import type { FeatureStoreState } from '@/app/components/base/features/store' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' + +const mockUseContext = jest.fn() +jest.mock('use-context-selector', () => { + const actual = jest.requireActual('use-context-selector') + return { + ...actual, + useContext: (context: unknown) => mockUseContext(context), + } +}) + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +const mockUseFeatures = jest.fn() +const mockUseFeaturesStore = jest.fn() +jest.mock('@/app/components/base/features/hooks', () => ({ + useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector), + useFeaturesStore: () => mockUseFeaturesStore(), +})) + +type SetupOptions = { + isVisible?: boolean + allowedTypes?: SupportUploadFileTypes[] +} + +let mockFeatureStoreState: FeatureStoreState +let mockSetFeatures: jest.Mock +const mockStore = { + getState: jest.fn(() => mockFeatureStoreState), +} + +const setupFeatureStore = (allowedTypes: SupportUploadFileTypes[] = []) => { + mockSetFeatures = jest.fn() + mockFeatureStoreState = { + features: { + file: { + allowed_file_types: allowedTypes, + enabled: allowedTypes.length > 0, + }, + }, + setFeatures: mockSetFeatures, + showFeaturesModal: false, + setShowFeaturesModal: jest.fn(), + } + mockStore.getState.mockImplementation(() => mockFeatureStoreState) + mockUseFeaturesStore.mockReturnValue(mockStore) + mockUseFeatures.mockImplementation(selector => selector(mockFeatureStoreState)) +} + +const renderConfigAudio = (options: SetupOptions = {}) => { + const { + isVisible = true, + allowedTypes = [], + } = options + setupFeatureStore(allowedTypes) + mockUseContext.mockReturnValue({ + isShowAudioConfig: isVisible, + }) + const user = userEvent.setup() + render() + return { + user, + setFeatures: mockSetFeatures, + } +} + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('ConfigAudio', () => { + it('should not render when the audio configuration is hidden', () => { + renderConfigAudio({ isVisible: false }) + + expect(screen.queryByText('appDebug.feature.audioUpload.title')).not.toBeInTheDocument() + }) + + it('should display the audio toggle state based on feature store data', () => { + renderConfigAudio({ allowedTypes: [SupportUploadFileTypes.audio] }) + + expect(screen.getByText('appDebug.feature.audioUpload.title')).toBeInTheDocument() + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true') + }) + + it('should enable audio uploads when toggled on', async () => { + const { user, setFeatures } = renderConfigAudio() + const toggle = screen.getByRole('switch') + + expect(toggle).toHaveAttribute('aria-checked', 'false') + await user.click(toggle) + + expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({ + file: expect.objectContaining({ + allowed_file_types: [SupportUploadFileTypes.audio], + enabled: true, + }), + })) + }) + + it('should disable audio uploads and turn off file feature when last type is removed', async () => { + const { user, setFeatures } = renderConfigAudio({ allowedTypes: [SupportUploadFileTypes.audio] }) + const toggle = screen.getByRole('switch') + + expect(toggle).toHaveAttribute('aria-checked', 'true') + await user.click(toggle) + + expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({ + file: expect.objectContaining({ + allowed_file_types: [], + enabled: false, + }), + })) + }) +}) diff --git a/web/app/components/app/configuration/config/config-document.spec.tsx b/web/app/components/app/configuration/config/config-document.spec.tsx new file mode 100644 index 0000000000..aeb504fdbd --- /dev/null +++ b/web/app/components/app/configuration/config/config-document.spec.tsx @@ -0,0 +1,119 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import ConfigDocument from './config-document' +import type { FeatureStoreState } from '@/app/components/base/features/store' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' + +const mockUseContext = jest.fn() +jest.mock('use-context-selector', () => { + const actual = jest.requireActual('use-context-selector') + return { + ...actual, + useContext: (context: unknown) => mockUseContext(context), + } +}) + +const mockUseFeatures = jest.fn() +const mockUseFeaturesStore = jest.fn() +jest.mock('@/app/components/base/features/hooks', () => ({ + useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector), + useFeaturesStore: () => mockUseFeaturesStore(), +})) + +type SetupOptions = { + isVisible?: boolean + allowedTypes?: SupportUploadFileTypes[] +} + +let mockFeatureStoreState: FeatureStoreState +let mockSetFeatures: jest.Mock +const mockStore = { + getState: jest.fn(() => mockFeatureStoreState), +} + +const setupFeatureStore = (allowedTypes: SupportUploadFileTypes[] = []) => { + mockSetFeatures = jest.fn() + mockFeatureStoreState = { + features: { + file: { + allowed_file_types: allowedTypes, + enabled: allowedTypes.length > 0, + }, + }, + setFeatures: mockSetFeatures, + showFeaturesModal: false, + setShowFeaturesModal: jest.fn(), + } + mockStore.getState.mockImplementation(() => mockFeatureStoreState) + mockUseFeaturesStore.mockReturnValue(mockStore) + mockUseFeatures.mockImplementation(selector => selector(mockFeatureStoreState)) +} + +const renderConfigDocument = (options: SetupOptions = {}) => { + const { + isVisible = true, + allowedTypes = [], + } = options + setupFeatureStore(allowedTypes) + mockUseContext.mockReturnValue({ + isShowDocumentConfig: isVisible, + }) + const user = userEvent.setup() + render() + return { + user, + setFeatures: mockSetFeatures, + } +} + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('ConfigDocument', () => { + it('should not render when the document configuration is hidden', () => { + renderConfigDocument({ isVisible: false }) + + expect(screen.queryByText('appDebug.feature.documentUpload.title')).not.toBeInTheDocument() + }) + + it('should show document toggle badge when configuration is visible', () => { + renderConfigDocument({ allowedTypes: [SupportUploadFileTypes.document] }) + + expect(screen.getByText('appDebug.feature.documentUpload.title')).toBeInTheDocument() + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true') + }) + + it('should add document type to allowed list when toggled on', async () => { + const { user, setFeatures } = renderConfigDocument({ allowedTypes: [SupportUploadFileTypes.audio] }) + const toggle = screen.getByRole('switch') + + expect(toggle).toHaveAttribute('aria-checked', 'false') + await user.click(toggle) + + expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({ + file: expect.objectContaining({ + allowed_file_types: [SupportUploadFileTypes.audio, SupportUploadFileTypes.document], + enabled: true, + }), + })) + }) + + it('should remove document type but keep file feature enabled when other types remain', async () => { + const { user, setFeatures } = renderConfigDocument({ + allowedTypes: [SupportUploadFileTypes.document, SupportUploadFileTypes.audio], + }) + const toggle = screen.getByRole('switch') + + expect(toggle).toHaveAttribute('aria-checked', 'true') + await user.click(toggle) + + expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({ + file: expect.objectContaining({ + allowed_file_types: [SupportUploadFileTypes.audio], + enabled: true, + }), + })) + }) +}) diff --git a/web/app/components/app/configuration/config/index.spec.tsx b/web/app/components/app/configuration/config/index.spec.tsx new file mode 100644 index 0000000000..814c52c3d7 --- /dev/null +++ b/web/app/components/app/configuration/config/index.spec.tsx @@ -0,0 +1,254 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import Config from './index' +import type { ModelConfig, PromptVariable } from '@/models/debug' +import * as useContextSelector from 'use-context-selector' +import type { ToolItem } from '@/types/app' +import { AgentStrategy, AppModeEnum, ModelModeType } from '@/types/app' + +jest.mock('use-context-selector', () => { + const actual = jest.requireActual('use-context-selector') + return { + ...actual, + useContext: jest.fn(), + } +}) + +const mockFormattingDispatcher = jest.fn() +jest.mock('../debug/hooks', () => ({ + __esModule: true, + useFormattingChangedDispatcher: () => mockFormattingDispatcher, +})) + +let latestConfigPromptProps: any +jest.mock('@/app/components/app/configuration/config-prompt', () => ({ + __esModule: true, + default: (props: any) => { + latestConfigPromptProps = props + return
+ }, +})) + +let latestConfigVarProps: any +jest.mock('@/app/components/app/configuration/config-var', () => ({ + __esModule: true, + default: (props: any) => { + latestConfigVarProps = props + return
+ }, +})) + +jest.mock('../dataset-config', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('./agent/agent-tools', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('../config-vision', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('./config-document', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('./config-audio', () => ({ + __esModule: true, + default: () =>
, +})) + +let latestHistoryPanelProps: any +jest.mock('../config-prompt/conversation-history/history-panel', () => ({ + __esModule: true, + default: (props: any) => { + latestHistoryPanelProps = props + return
+ }, +})) + +type MockContext = { + mode: AppModeEnum + isAdvancedMode: boolean + modelModeType: ModelModeType + isAgent: boolean + hasSetBlockStatus: { + context: boolean + history: boolean + query: boolean + } + showHistoryModal: jest.Mock + modelConfig: ModelConfig + setModelConfig: jest.Mock + setPrevPromptConfig: jest.Mock +} + +const createPromptVariable = (overrides: Partial = {}): PromptVariable => ({ + key: 'variable', + name: 'Variable', + type: 'string', + ...overrides, +}) + +const createModelConfig = (overrides: Partial = {}): ModelConfig => ({ + provider: 'openai', + model_id: 'gpt-4', + mode: ModelModeType.chat, + configs: { + prompt_template: 'Hello {{variable}}', + prompt_variables: [createPromptVariable({ key: 'existing' })], + }, + chat_prompt_config: null, + completion_prompt_config: null, + opening_statement: null, + more_like_this: null, + suggested_questions: null, + suggested_questions_after_answer: null, + speech_to_text: null, + text_to_speech: null, + file_upload: null, + retriever_resource: null, + sensitive_word_avoidance: null, + annotation_reply: null, + external_data_tools: null, + system_parameters: { + audio_file_size_limit: 1, + file_size_limit: 1, + image_file_size_limit: 1, + video_file_size_limit: 1, + workflow_file_upload_limit: 1, + }, + dataSets: [], + agentConfig: { + enabled: false, + strategy: AgentStrategy.react, + max_iteration: 1, + tools: [] as ToolItem[], + }, + ...overrides, +}) + +const createContextValue = (overrides: Partial = {}): MockContext => ({ + mode: AppModeEnum.CHAT, + isAdvancedMode: false, + modelModeType: ModelModeType.chat, + isAgent: false, + hasSetBlockStatus: { + context: false, + history: true, + query: false, + }, + showHistoryModal: jest.fn(), + modelConfig: createModelConfig(), + setModelConfig: jest.fn(), + setPrevPromptConfig: jest.fn(), + ...overrides, +}) + +const mockUseContext = useContextSelector.useContext as jest.Mock + +const renderConfig = (contextOverrides: Partial = {}) => { + const contextValue = createContextValue(contextOverrides) + mockUseContext.mockReturnValue(contextValue) + return { + contextValue, + ...render(), + } +} + +beforeEach(() => { + jest.clearAllMocks() + latestConfigPromptProps = undefined + latestConfigVarProps = undefined + latestHistoryPanelProps = undefined +}) + +// Rendering scenarios ensure the layout toggles agent/history specific sections correctly. +describe('Config - Rendering', () => { + it('should render baseline sections without agent specific panels', () => { + renderConfig() + + expect(screen.getByTestId('config-prompt')).toBeInTheDocument() + expect(screen.getByTestId('config-var')).toBeInTheDocument() + expect(screen.getByTestId('dataset-config')).toBeInTheDocument() + expect(screen.getByTestId('config-vision')).toBeInTheDocument() + expect(screen.getByTestId('config-document')).toBeInTheDocument() + expect(screen.getByTestId('config-audio')).toBeInTheDocument() + expect(screen.queryByTestId('agent-tools')).not.toBeInTheDocument() + expect(screen.queryByTestId('history-panel')).not.toBeInTheDocument() + }) + + it('should show AgentTools when app runs in agent mode', () => { + renderConfig({ isAgent: true }) + + expect(screen.getByTestId('agent-tools')).toBeInTheDocument() + }) + + it('should display HistoryPanel only when advanced chat completion values apply', () => { + const showHistoryModal = jest.fn() + renderConfig({ + isAdvancedMode: true, + mode: AppModeEnum.ADVANCED_CHAT, + modelModeType: ModelModeType.completion, + hasSetBlockStatus: { + context: false, + history: false, + query: false, + }, + showHistoryModal, + }) + + expect(screen.getByTestId('history-panel')).toBeInTheDocument() + expect(latestHistoryPanelProps.showWarning).toBe(true) + expect(latestHistoryPanelProps.onShowEditModal).toBe(showHistoryModal) + }) +}) + +// Prompt handling scenarios validate integration between Config and prompt children. +describe('Config - Prompt Handling', () => { + it('should update prompt template and dispatch formatting event when text changes', () => { + const { contextValue } = renderConfig() + const previousVariables = contextValue.modelConfig.configs.prompt_variables + const additions = [createPromptVariable({ key: 'new', name: 'New' })] + + latestConfigPromptProps.onChange('Updated template', additions) + + expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs) + expect(contextValue.setModelConfig).toHaveBeenCalledWith(expect.objectContaining({ + configs: expect.objectContaining({ + prompt_template: 'Updated template', + prompt_variables: [...previousVariables, ...additions], + }), + })) + expect(mockFormattingDispatcher).toHaveBeenCalledTimes(1) + }) + + it('should skip formatting dispatcher when template remains identical', () => { + const { contextValue } = renderConfig() + const unchangedTemplate = contextValue.modelConfig.configs.prompt_template + + latestConfigPromptProps.onChange(unchangedTemplate, [createPromptVariable({ key: 'added' })]) + + expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs) + expect(mockFormattingDispatcher).not.toHaveBeenCalled() + }) + + it('should replace prompt variables when ConfigVar reports updates', () => { + const { contextValue } = renderConfig() + const replacementVariables = [createPromptVariable({ key: 'replacement' })] + + latestConfigVarProps.onPromptVariablesChange(replacementVariables) + + expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs) + expect(contextValue.setModelConfig).toHaveBeenCalledWith(expect.objectContaining({ + configs: expect.objectContaining({ + prompt_variables: replacementVariables, + }), + })) + }) +}) From 82220a645cba9553322dad8e9b705c695e6c5a45 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 18 Dec 2025 18:30:58 +0900 Subject: [PATCH 03/17] refactor: split changes for api/controllers/web/audio.py (#29856) Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/web/audio.py | 39 +++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/api/controllers/web/audio.py b/api/controllers/web/audio.py index b9fef48c4d..15828cc208 100644 --- a/api/controllers/web/audio.py +++ b/api/controllers/web/audio.py @@ -1,7 +1,8 @@ import logging from flask import request -from flask_restx import fields, marshal_with, reqparse +from flask_restx import fields, marshal_with +from pydantic import BaseModel, field_validator from werkzeug.exceptions import InternalServerError import services @@ -20,6 +21,7 @@ from controllers.web.error import ( from controllers.web.wraps import WebApiResource from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.model_runtime.errors.invoke import InvokeError +from libs.helper import uuid_value from models.model import App from services.audio_service import AudioService from services.errors.audio import ( @@ -29,6 +31,25 @@ from services.errors.audio import ( UnsupportedAudioTypeServiceError, ) +from ..common.schema import register_schema_models + + +class TextToAudioPayload(BaseModel): + message_id: str | None = None + voice: str | None = None + text: str | None = None + streaming: bool | None = None + + @field_validator("message_id") + @classmethod + def validate_message_id(cls, value: str | None) -> str | None: + if value is None: + return value + return uuid_value(value) + + +register_schema_models(web_ns, TextToAudioPayload) + logger = logging.getLogger(__name__) @@ -88,6 +109,7 @@ class AudioApi(WebApiResource): @web_ns.route("/text-to-audio") class TextApi(WebApiResource): + @web_ns.expect(web_ns.models[TextToAudioPayload.__name__]) @web_ns.doc("Text to Audio") @web_ns.doc(description="Convert text to audio using text-to-speech service.") @web_ns.doc( @@ -102,18 +124,11 @@ class TextApi(WebApiResource): def post(self, app_model: App, end_user): """Convert text to audio""" try: - parser = ( - reqparse.RequestParser() - .add_argument("message_id", type=str, required=False, location="json") - .add_argument("voice", type=str, location="json") - .add_argument("text", type=str, location="json") - .add_argument("streaming", type=bool, location="json") - ) - args = parser.parse_args() + payload = TextToAudioPayload.model_validate(web_ns.payload or {}) - message_id = args.get("message_id", None) - text = args.get("text", None) - voice = args.get("voice", None) + message_id = payload.message_id + text = payload.text + voice = payload.voice response = AudioService.transcript_tts( app_model=app_model, text=text, voice=voice, end_user=end_user.external_user_id, message_id=message_id ) From c12f0d16bb20572906faf19e0eba85528756b110 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:47:13 +0800 Subject: [PATCH 04/17] chore(web): enhance frontend tests (#29869) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .github/workflows/web-tests.yml | 108 +++-- .../assistant-type-picker/index.spec.tsx | 204 +++++---- .../debug-with-single-model/index.spec.tsx | 399 +++++++++--------- .../billing/upgrade-btn/index.spec.tsx | 174 ++------ .../explore/installed-app/index.spec.tsx | 77 +--- web/jest.config.ts | 1 + web/jest.setup.ts | 16 + web/package.json | 1 + web/pnpm-lock.yaml | 3 + 9 files changed, 434 insertions(+), 549 deletions(-) diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index dd311701b5..8b871403cc 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -70,6 +70,13 @@ jobs: node <<'NODE' >> "$GITHUB_STEP_SUMMARY" const fs = require('fs'); const path = require('path'); + let libCoverage = null; + + try { + libCoverage = require('istanbul-lib-coverage'); + } catch (error) { + libCoverage = null; + } const summaryPath = path.join('coverage', 'coverage-summary.json'); const finalPath = path.join('coverage', 'coverage-final.json'); @@ -91,6 +98,54 @@ jobs: ? JSON.parse(fs.readFileSync(finalPath, 'utf8')) : null; + const getLineCoverageFromStatements = (statementMap, statementHits) => { + const lineHits = {}; + + if (!statementMap || !statementHits) { + return lineHits; + } + + Object.entries(statementMap).forEach(([key, statement]) => { + const line = statement?.start?.line; + if (!line) { + return; + } + const hits = statementHits[key] ?? 0; + const previous = lineHits[line]; + lineHits[line] = previous === undefined ? hits : Math.max(previous, hits); + }); + + return lineHits; + }; + + const getFileCoverage = (entry) => ( + libCoverage ? libCoverage.createFileCoverage(entry) : null + ); + + const getLineHits = (entry, fileCoverage) => { + const lineHits = entry.l ?? {}; + if (Object.keys(lineHits).length > 0) { + return lineHits; + } + if (fileCoverage) { + return fileCoverage.getLineCoverage(); + } + return getLineCoverageFromStatements(entry.statementMap ?? {}, entry.s ?? {}); + }; + + const getUncoveredLines = (entry, fileCoverage, lineHits) => { + if (lineHits && Object.keys(lineHits).length > 0) { + return Object.entries(lineHits) + .filter(([, count]) => count === 0) + .map(([line]) => Number(line)) + .sort((a, b) => a - b); + } + if (fileCoverage) { + return fileCoverage.getUncoveredLines(); + } + return []; + }; + const totals = { lines: { covered: 0, total: 0 }, statements: { covered: 0, total: 0 }, @@ -106,7 +161,7 @@ jobs: totals[key].covered = totalEntry[key].covered ?? 0; totals[key].total = totalEntry[key].total ?? 0; } - }); + }); Object.entries(summary) .filter(([file]) => file !== 'total') @@ -122,7 +177,8 @@ jobs: }); } else if (coverage) { Object.entries(coverage).forEach(([file, entry]) => { - const lineHits = entry.l ?? {}; + const fileCoverage = getFileCoverage(entry); + const lineHits = getLineHits(entry, fileCoverage); const statementHits = entry.s ?? {}; const branchHits = entry.b ?? {}; const functionHits = entry.f ?? {}; @@ -228,7 +284,8 @@ jobs: }; const tableRows = Object.entries(coverage) .map(([file, entry]) => { - const lineHits = entry.l ?? {}; + const fileCoverage = getFileCoverage(entry); + const lineHits = getLineHits(entry, fileCoverage); const statementHits = entry.s ?? {}; const branchHits = entry.b ?? {}; const functionHits = entry.f ?? {}; @@ -254,10 +311,7 @@ jobs: tableTotals.functions.total += functionTotal; tableTotals.functions.covered += functionCovered; - const uncoveredLines = Object.entries(lineHits) - .filter(([, count]) => count === 0) - .map(([line]) => Number(line)) - .sort((a, b) => a - b); + const uncoveredLines = getUncoveredLines(entry, fileCoverage, lineHits); const filePath = entry.path ?? file; const relativePath = path.isAbsolute(filePath) @@ -294,46 +348,20 @@ jobs: }; const rowsForOutput = [allFilesRow, ...tableRows]; - const columnWidths = Object.fromEntries( - columns.map(({ key, header }) => [key, header.length]), - ); - - rowsForOutput.forEach((row) => { - columns.forEach(({ key }) => { - const value = String(row[key] ?? ''); - columnWidths[key] = Math.max(columnWidths[key], value.length); - }); - }); - - const formatRow = (row) => columns - .map(({ key, align }) => { - const value = String(row[key] ?? ''); - const width = columnWidths[key]; - return align === 'right' ? value.padStart(width) : value.padEnd(width); - }) - .join(' | '); - - const headerRow = columns - .map(({ header, key, align }) => { - const width = columnWidths[key]; - return align === 'right' ? header.padStart(width) : header.padEnd(width); - }) - .join(' | '); - - const dividerRow = columns - .map(({ key }) => '-'.repeat(columnWidths[key])) - .join('|'); + const formatRow = (row) => `| ${columns + .map(({ key }) => String(row[key] ?? '')) + .join(' | ')} |`; + const headerRow = `| ${columns.map(({ header }) => header).join(' | ')} |`; + const dividerRow = `| ${columns + .map(({ align }) => (align === 'right' ? '---:' : ':---')) + .join(' | ')} |`; console.log(''); console.log('
Jest coverage table'); console.log(''); - console.log('```'); - console.log(dividerRow); console.log(headerRow); console.log(dividerRow); rowsForOutput.forEach((row) => console.log(formatRow(row))); - console.log(dividerRow); - console.log('```'); console.log('
'); } NODE diff --git a/web/app/components/app/configuration/config/assistant-type-picker/index.spec.tsx b/web/app/components/app/configuration/config/assistant-type-picker/index.spec.tsx index f935a203fe..cda24ea045 100644 --- a/web/app/components/app/configuration/config/assistant-type-picker/index.spec.tsx +++ b/web/app/components/app/configuration/config/assistant-type-picker/index.spec.tsx @@ -5,31 +5,6 @@ import AssistantTypePicker from './index' import type { AgentConfig } from '@/models/debug' import { AgentStrategy } from '@/types/app' -// Type definition for AgentSetting props -type AgentSettingProps = { - isChatModel: boolean - payload: AgentConfig - isFunctionCall: boolean - onCancel: () => void - onSave: (payload: AgentConfig) => void -} - -// Track mock calls for props validation -let mockAgentSettingProps: AgentSettingProps | null = null - -// Mock AgentSetting component (complex modal with external hooks) -jest.mock('../agent/agent-setting', () => { - return function MockAgentSetting(props: AgentSettingProps) { - mockAgentSettingProps = props - return ( -
- - -
- ) - } -}) - // Test utilities const defaultAgentConfig: AgentConfig = { enabled: true, @@ -62,7 +37,6 @@ const getOptionByDescription = (descriptionRegex: RegExp) => { describe('AssistantTypePicker', () => { beforeEach(() => { jest.clearAllMocks() - mockAgentSettingProps = null }) // Rendering tests (REQUIRED) @@ -139,8 +113,8 @@ describe('AssistantTypePicker', () => { renderComponent() // Act - const trigger = screen.getByText(/chatAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/chatAssistant.name/i) + await user.click(trigger) // Assert - Both options should be visible await waitFor(() => { @@ -225,8 +199,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'chat' }) // Act - Open dropdown - const trigger = screen.getByText(/chatAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/chatAssistant.name/i) + await user.click(trigger) // Wait for dropdown and select agent await waitFor(() => { @@ -235,7 +209,7 @@ describe('AssistantTypePicker', () => { }) const agentOptions = screen.getAllByText(/agentAssistant.name/i) - await user.click(agentOptions[0].closest('div')!) + await user.click(agentOptions[0]) // Assert - Dropdown should remain open (agent settings should be visible) await waitFor(() => { @@ -250,8 +224,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'chat', onChange }) // Act - Open dropdown - const trigger = screen.getByText(/chatAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/chatAssistant.name/i) + await user.click(trigger) // Wait for dropdown and click same option await waitFor(() => { @@ -260,7 +234,7 @@ describe('AssistantTypePicker', () => { }) const chatOptions = screen.getAllByText(/chatAssistant.name/i) - await user.click(chatOptions[1].closest('div')!) + await user.click(chatOptions[1]) // Assert expect(onChange).not.toHaveBeenCalled() @@ -276,8 +250,8 @@ describe('AssistantTypePicker', () => { renderComponent({ disabled: true, onChange }) // Act - Open dropdown (dropdown can still open when disabled) - const trigger = screen.getByText(/chatAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/chatAssistant.name/i) + await user.click(trigger) // Wait for dropdown to open await waitFor(() => { @@ -298,8 +272,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent', disabled: true }) // Act - Open dropdown - const trigger = screen.getByText(/agentAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/agentAssistant.name/i) + await user.click(trigger) // Assert - Agent settings option should not be visible await waitFor(() => { @@ -313,8 +287,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent', disabled: false }) // Act - Open dropdown - const trigger = screen.getByText(/agentAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/agentAssistant.name/i) + await user.click(trigger) // Assert - Agent settings option should be visible await waitFor(() => { @@ -331,20 +305,20 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent', disabled: false }) // Act - Open dropdown - const trigger = screen.getByText(/agentAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/agentAssistant.name/i) + await user.click(trigger) // Click agent settings await waitFor(() => { expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() }) - const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div') - await user.click(agentSettingsTrigger!) + const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) + await user.click(agentSettingsTrigger) // Assert await waitFor(() => { - expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() }) }) @@ -354,8 +328,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'chat', disabled: false }) // Act - Open dropdown - const trigger = screen.getByText(/chatAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/chatAssistant.name/i) + await user.click(trigger) // Wait for dropdown to open await waitFor(() => { @@ -363,7 +337,7 @@ describe('AssistantTypePicker', () => { }) // Assert - Agent settings modal should not appear (value is 'chat') - expect(screen.queryByTestId('agent-setting-modal')).not.toBeInTheDocument() + expect(screen.queryByText(/common.operation.save/i)).not.toBeInTheDocument() }) it('should call onAgentSettingChange when saving agent settings', async () => { @@ -373,26 +347,26 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent', disabled: false, onAgentSettingChange }) // Act - Open dropdown and agent settings - const trigger = screen.getByText(/agentAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/agentAssistant.name/i) + await user.click(trigger) await waitFor(() => { expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() }) - const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div') - await user.click(agentSettingsTrigger!) + const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) + await user.click(agentSettingsTrigger) // Wait for modal and click save await waitFor(() => { - expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() }) - const saveButton = screen.getByText('Save') + const saveButton = screen.getByText(/common.operation.save/i) await user.click(saveButton) // Assert - expect(onAgentSettingChange).toHaveBeenCalledWith({ max_iteration: 5 }) + expect(onAgentSettingChange).toHaveBeenCalledWith(defaultAgentConfig) }) it('should close modal when saving agent settings', async () => { @@ -401,26 +375,26 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent', disabled: false }) // Act - Open dropdown, agent settings, and save - const trigger = screen.getByText(/agentAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/agentAssistant.name/i) + await user.click(trigger) await waitFor(() => { expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() }) - const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div') - await user.click(agentSettingsTrigger!) + const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) + await user.click(agentSettingsTrigger) await waitFor(() => { - expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument() + expect(screen.getByText(/appDebug.agent.setting.name/i)).toBeInTheDocument() }) - const saveButton = screen.getByText('Save') + const saveButton = screen.getByText(/common.operation.save/i) await user.click(saveButton) // Assert await waitFor(() => { - expect(screen.queryByTestId('agent-setting-modal')).not.toBeInTheDocument() + expect(screen.queryByText(/common.operation.save/i)).not.toBeInTheDocument() }) }) @@ -431,26 +405,26 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent', disabled: false, onAgentSettingChange }) // Act - Open dropdown, agent settings, and cancel - const trigger = screen.getByText(/agentAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/agentAssistant.name/i) + await user.click(trigger) await waitFor(() => { expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() }) - const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div') - await user.click(agentSettingsTrigger!) + const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) + await user.click(agentSettingsTrigger) await waitFor(() => { - expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() }) - const cancelButton = screen.getByText('Cancel') + const cancelButton = screen.getByText(/common.operation.cancel/i) await user.click(cancelButton) // Assert await waitFor(() => { - expect(screen.queryByTestId('agent-setting-modal')).not.toBeInTheDocument() + expect(screen.queryByText(/common.operation.save/i)).not.toBeInTheDocument() }) expect(onAgentSettingChange).not.toHaveBeenCalled() }) @@ -461,19 +435,19 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent', disabled: false }) // Act - Open dropdown and agent settings - const trigger = screen.getByText(/agentAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/agentAssistant.name/i) + await user.click(trigger) await waitFor(() => { expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() }) - const agentSettingsTrigger = screen.getByText(/agent.setting.name/i).closest('div') - await user.click(agentSettingsTrigger!) + const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) + await user.click(agentSettingsTrigger) // Assert - Modal should be open and dropdown should close await waitFor(() => { - expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() }) // The dropdown should be closed (agent settings description should not be visible) @@ -492,10 +466,10 @@ describe('AssistantTypePicker', () => { renderComponent() // Act - const trigger = screen.getByText(/chatAssistant.name/i).closest('div') - await user.click(trigger!) - await user.click(trigger!) - await user.click(trigger!) + const trigger = screen.getByText(/chatAssistant.name/i) + await user.click(trigger) + await user.click(trigger) + await user.click(trigger) // Assert - Should not crash expect(trigger).toBeInTheDocument() @@ -538,8 +512,8 @@ describe('AssistantTypePicker', () => { }) }).not.toThrow() - const trigger = screen.getByText(/chatAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/chatAssistant.name/i) + await user.click(trigger) }) it('should handle empty agentConfig', async () => { @@ -630,8 +604,8 @@ describe('AssistantTypePicker', () => { renderComponent() // Act - Open dropdown - const trigger = screen.getByText(/chatAssistant.name/i).closest('div') - await user.click(trigger!) + const trigger = screen.getByText(/chatAssistant.name/i) + await user.click(trigger) // Assert - Descriptions should be visible await waitFor(() => { @@ -657,18 +631,14 @@ describe('AssistantTypePicker', () => { }) }) - // Props Validation for AgentSetting - describe('AgentSetting Props', () => { - it('should pass isFunctionCall and isChatModel props to AgentSetting', async () => { + // Agent Setting Integration + describe('AgentSetting Integration', () => { + it('should show function call mode when isFunctionCall is true', async () => { // Arrange const user = userEvent.setup() - renderComponent({ - value: 'agent', - isFunctionCall: true, - isChatModel: false, - }) + renderComponent({ value: 'agent', isFunctionCall: true, isChatModel: false }) - // Act - Open dropdown and trigger AgentSetting + // Act - Open dropdown and settings modal const trigger = screen.getByText(/agentAssistant.name/i) await user.click(trigger) @@ -679,17 +649,37 @@ describe('AssistantTypePicker', () => { const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) await user.click(agentSettingsTrigger) - // Assert - Verify AgentSetting receives correct props + // Assert await waitFor(() => { - expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() }) - - expect(mockAgentSettingProps).not.toBeNull() - expect(mockAgentSettingProps!.isFunctionCall).toBe(true) - expect(mockAgentSettingProps!.isChatModel).toBe(false) + expect(screen.getByText(/appDebug.agent.agentModeType.functionCall/i)).toBeInTheDocument() }) - it('should pass agentConfig payload to AgentSetting', async () => { + it('should show built-in prompt when isFunctionCall is false', async () => { + // Arrange + const user = userEvent.setup() + renderComponent({ value: 'agent', isFunctionCall: false, isChatModel: true }) + + // Act - Open dropdown and settings modal + const trigger = screen.getByText(/agentAssistant.name/i) + await user.click(trigger) + + await waitFor(() => { + expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + }) + + const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) + await user.click(agentSettingsTrigger) + + // Assert + await waitFor(() => { + expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() + }) + expect(screen.getByText(/tools.builtInPromptTitle/i)).toBeInTheDocument() + }) + + it('should initialize max iteration from agentConfig payload', async () => { // Arrange const user = userEvent.setup() const customConfig: AgentConfig = { @@ -699,12 +689,9 @@ describe('AssistantTypePicker', () => { tools: [], } - renderComponent({ - value: 'agent', - agentConfig: customConfig, - }) + renderComponent({ value: 'agent', agentConfig: customConfig }) - // Act - Open AgentSetting + // Act - Open dropdown and settings modal const trigger = screen.getByText(/agentAssistant.name/i) await user.click(trigger) @@ -715,13 +702,10 @@ describe('AssistantTypePicker', () => { const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) await user.click(agentSettingsTrigger) - // Assert - Verify payload was passed - await waitFor(() => { - expect(screen.getByTestId('agent-setting-modal')).toBeInTheDocument() - }) - - expect(mockAgentSettingProps).not.toBeNull() - expect(mockAgentSettingProps!.payload).toEqual(customConfig) + // Assert + await screen.findByText(/common.operation.save/i) + const maxIterationInput = await screen.findByRole('spinbutton') + expect(maxIterationInput).toHaveValue(10) }) }) diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx index f76145f901..676456c3ea 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/index.spec.tsx @@ -1,5 +1,5 @@ -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import { createRef } from 'react' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { type ReactNode, type RefObject, createRef } from 'react' import DebugWithSingleModel from './index' import type { DebugWithSingleModelRefType } from './index' import type { ChatItem } from '@/app/components/base/chat/types' @@ -8,7 +8,8 @@ import type { ProviderContextState } from '@/context/provider-context' import type { DatasetConfigs, ModelConfig } from '@/models/debug' import { PromptMode } from '@/models/debug' import { type Collection, CollectionType } from '@/app/components/tools/types' -import { AgentStrategy, AppModeEnum, ModelModeType } from '@/types/app' +import type { FileEntity } from '@/app/components/base/file-uploader/types' +import { AgentStrategy, AppModeEnum, ModelModeType, Resolution, TransferMethod } from '@/types/app' // ============================================================================ // Test Data Factories (Following testing.md guidelines) @@ -67,21 +68,6 @@ function createMockModelConfig(overrides: Partial = {}): ModelConfi } } -/** - * Factory function for creating mock ChatItem list - * Note: Currently unused but kept for potential future test cases - */ -// eslint-disable-next-line unused-imports/no-unused-vars -function createMockChatList(items: Partial[] = []): ChatItem[] { - return items.map((item, index) => ({ - id: `msg-${index}`, - content: 'Test message', - isAnswer: false, - message_files: [], - ...item, - })) -} - /** * Factory function for creating mock Collection list */ @@ -156,9 +142,9 @@ const mockFetchSuggestedQuestions = jest.fn() const mockStopChatMessageResponding = jest.fn() jest.mock('@/service/debug', () => ({ - fetchConversationMessages: (...args: any[]) => mockFetchConversationMessages(...args), - fetchSuggestedQuestions: (...args: any[]) => mockFetchSuggestedQuestions(...args), - stopChatMessageResponding: (...args: any[]) => mockStopChatMessageResponding(...args), + fetchConversationMessages: (...args: unknown[]) => mockFetchConversationMessages(...args), + fetchSuggestedQuestions: (...args: unknown[]) => mockFetchSuggestedQuestions(...args), + stopChatMessageResponding: (...args: unknown[]) => mockStopChatMessageResponding(...args), })) jest.mock('next/navigation', () => ({ @@ -255,11 +241,11 @@ const mockDebugConfigContext = { score_threshold: 0.7, datasets: { datasets: [] }, } as DatasetConfigs, - datasetConfigsRef: { current: null } as any, + datasetConfigsRef: createRef(), setDatasetConfigs: jest.fn(), hasSetContextVar: false, isShowVisionConfig: false, - visionConfig: { enabled: false, number_limits: 2, detail: 'low' as any, transfer_methods: [] }, + visionConfig: { enabled: false, number_limits: 2, detail: Resolution.low, transfer_methods: [] }, setVisionConfig: jest.fn(), isAllowVideoUpload: false, isShowDocumentConfig: false, @@ -295,7 +281,19 @@ jest.mock('@/context/app-context', () => ({ useAppContext: jest.fn(() => mockAppContext), })) -const mockFeatures = { +type FeatureState = { + moreLikeThis: { enabled: boolean } + opening: { enabled: boolean; opening_statement: string; suggested_questions: string[] } + moderation: { enabled: boolean } + speech2text: { enabled: boolean } + text2speech: { enabled: boolean } + file: { enabled: boolean } + suggested: { enabled: boolean } + citation: { enabled: boolean } + annotationReply: { enabled: boolean } +} + +const defaultFeatures: FeatureState = { moreLikeThis: { enabled: false }, opening: { enabled: false, opening_statement: '', suggested_questions: [] }, moderation: { enabled: false }, @@ -306,13 +304,11 @@ const mockFeatures = { citation: { enabled: false }, annotationReply: { enabled: false }, } +type FeatureSelector = (state: { features: FeatureState }) => unknown +let mockFeaturesState: FeatureState = { ...defaultFeatures } jest.mock('@/app/components/base/features/hooks', () => ({ - useFeatures: jest.fn((selector) => { - if (typeof selector === 'function') - return selector({ features: mockFeatures }) - return mockFeatures - }), + useFeatures: jest.fn(), })) const mockConfigFromDebugContext = { @@ -345,7 +341,7 @@ jest.mock('../hooks', () => ({ const mockSetShowAppConfigureFeaturesModal = jest.fn() jest.mock('@/app/components/app/store', () => ({ - useStore: jest.fn((selector) => { + useStore: jest.fn((selector?: (state: { setShowAppConfigureFeaturesModal: typeof mockSetShowAppConfigureFeaturesModal }) => unknown) => { if (typeof selector === 'function') return selector({ setShowAppConfigureFeaturesModal: mockSetShowAppConfigureFeaturesModal }) return mockSetShowAppConfigureFeaturesModal @@ -384,12 +380,31 @@ jest.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({ }, })) -// Mock external APIs that might be used -globalThis.ResizeObserver = jest.fn().mockImplementation(() => ({ - observe: jest.fn(), - unobserve: jest.fn(), - disconnect: jest.fn(), -})) +type MockChatProps = { + chatList?: ChatItem[] + isResponding?: boolean + onSend?: (message: string, files?: FileEntity[]) => void + onRegenerate?: (chatItem: ChatItem, editedQuestion?: { message: string; files?: FileEntity[] }) => void + onStopResponding?: () => void + suggestedQuestions?: string[] + questionIcon?: ReactNode + answerIcon?: ReactNode + onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void + onAnnotationEdited?: (question: string, answer: string, index: number) => void + onAnnotationRemoved?: (index: number) => void + switchSibling?: (siblingMessageId: string) => void + onFeatureBarClick?: (state: boolean) => void +} + +const mockFile: FileEntity = { + id: 'file-1', + name: 'test.png', + size: 123, + type: 'image/png', + progress: 100, + transferMethod: TransferMethod.local_file, + supportFileType: 'image', +} // Mock Chat component (complex with many dependencies) // This is a pragmatic mock that tests the integration at DebugWithSingleModel level @@ -408,11 +423,13 @@ jest.mock('@/app/components/base/chat/chat', () => { onAnnotationRemoved, switchSibling, onFeatureBarClick, - }: any) { + }: MockChatProps) { + const items = chatList || [] + const suggested = suggestedQuestions ?? [] return (
- {chatList?.map((item: any) => ( + {items.map((item: ChatItem) => (
{item.content}
@@ -434,14 +451,21 @@ jest.mock('@/app/components/base/chat/chat', () => { > Send + {isResponding && ( )} - {suggestedQuestions?.length > 0 && ( + {suggested.length > 0 && (
- {suggestedQuestions.map((q: string, i: number) => ( + {suggested.map((q: string, i: number) => ( @@ -451,7 +475,13 @@ jest.mock('@/app/components/base/chat/chat', () => { {onRegenerate && ( @@ -506,12 +536,30 @@ jest.mock('@/app/components/base/chat/chat', () => { // ============================================================================ describe('DebugWithSingleModel', () => { - let ref: React.RefObject + let ref: RefObject beforeEach(() => { jest.clearAllMocks() ref = createRef() + const { useDebugConfigurationContext } = require('@/context/debug-configuration') + const { useProviderContext } = require('@/context/provider-context') + const { useAppContext } = require('@/context/app-context') + const { useConfigFromDebugContext, useFormattingChangedSubscription } = require('../hooks') + const { useFeatures } = require('@/app/components/base/features/hooks') as { useFeatures: jest.Mock } + + useDebugConfigurationContext.mockReturnValue(mockDebugConfigContext) + useProviderContext.mockReturnValue(mockProviderContext) + useAppContext.mockReturnValue(mockAppContext) + useConfigFromDebugContext.mockReturnValue(mockConfigFromDebugContext) + useFormattingChangedSubscription.mockReturnValue(undefined) + mockFeaturesState = { ...defaultFeatures } + useFeatures.mockImplementation((selector?: FeatureSelector) => { + if (typeof selector === 'function') + return selector({ features: mockFeaturesState }) + return mockFeaturesState + }) + // Reset mock implementations mockFetchConversationMessages.mockResolvedValue({ data: [] }) mockFetchSuggestedQuestions.mockResolvedValue({ data: [] }) @@ -521,7 +569,7 @@ describe('DebugWithSingleModel', () => { // Rendering Tests describe('Rendering', () => { it('should render without crashing', () => { - render(} />) + render(} />) // Verify Chat component is rendered expect(screen.getByTestId('chat-component')).toBeInTheDocument() @@ -532,7 +580,7 @@ describe('DebugWithSingleModel', () => { it('should render with custom checkCanSend prop', () => { const checkCanSend = jest.fn(() => true) - render(} checkCanSend={checkCanSend} />) + render(} checkCanSend={checkCanSend} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -543,122 +591,88 @@ describe('DebugWithSingleModel', () => { it('should respect checkCanSend returning true', async () => { const checkCanSend = jest.fn(() => true) - render(} checkCanSend={checkCanSend} />) + render(} checkCanSend={checkCanSend} />) const sendButton = screen.getByTestId('send-button') fireEvent.click(sendButton) + const { ssePost } = require('@/service/base') as { ssePost: jest.Mock } await waitFor(() => { expect(checkCanSend).toHaveBeenCalled() + expect(ssePost).toHaveBeenCalled() }) + + expect(ssePost.mock.calls[0][0]).toBe('apps/test-app-id/chat-messages') }) it('should prevent send when checkCanSend returns false', async () => { const checkCanSend = jest.fn(() => false) - render(} checkCanSend={checkCanSend} />) + render(} checkCanSend={checkCanSend} />) const sendButton = screen.getByTestId('send-button') fireEvent.click(sendButton) + const { ssePost } = require('@/service/base') as { ssePost: jest.Mock } await waitFor(() => { expect(checkCanSend).toHaveBeenCalled() expect(checkCanSend).toHaveReturnedWith(false) }) + expect(ssePost).not.toHaveBeenCalled() }) }) - // Context Integration Tests - describe('Context Integration', () => { - it('should use debug configuration context', () => { - const { useDebugConfigurationContext } = require('@/context/debug-configuration') + // User Interactions + describe('User Interactions', () => { + it('should open feature configuration when feature bar is clicked', () => { + render(} />) - render(} />) + fireEvent.click(screen.getByTestId('feature-bar-button')) - expect(useDebugConfigurationContext).toHaveBeenCalled() - }) - - it('should use provider context for model list', () => { - const { useProviderContext } = require('@/context/provider-context') - - render(} />) - - expect(useProviderContext).toHaveBeenCalled() - }) - - it('should use app context for user profile', () => { - const { useAppContext } = require('@/context/app-context') - - render(} />) - - expect(useAppContext).toHaveBeenCalled() - }) - - it('should use features from features hook', () => { - const { useFeatures } = require('@/app/components/base/features/hooks') - - render(} />) - - expect(useFeatures).toHaveBeenCalled() - }) - - it('should use config from debug context hook', () => { - const { useConfigFromDebugContext } = require('../hooks') - - render(} />) - - expect(useConfigFromDebugContext).toHaveBeenCalled() - }) - - it('should subscribe to formatting changes', () => { - const { useFormattingChangedSubscription } = require('../hooks') - - render(} />) - - expect(useFormattingChangedSubscription).toHaveBeenCalled() + expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalledWith(true) }) }) // Model Configuration Tests describe('Model Configuration', () => { - it('should merge features into config correctly when all features enabled', () => { - const { useFeatures } = require('@/app/components/base/features/hooks') + it('should include opening features in request when enabled', async () => { + mockFeaturesState = { + ...defaultFeatures, + opening: { enabled: true, opening_statement: 'Hello!', suggested_questions: ['Q1'] }, + } - useFeatures.mockReturnValue((selector: any) => { - const features = { - moreLikeThis: { enabled: true }, - opening: { enabled: true, opening_statement: 'Hello!', suggested_questions: ['Q1'] }, - moderation: { enabled: true }, - speech2text: { enabled: true }, - text2speech: { enabled: true }, - file: { enabled: true }, - suggested: { enabled: true }, - citation: { enabled: true }, - annotationReply: { enabled: true }, - } - return typeof selector === 'function' ? selector({ features }) : features + render(} />) + + fireEvent.click(screen.getByTestId('send-button')) + + const { ssePost } = require('@/service/base') as { ssePost: jest.Mock } + await waitFor(() => { + expect(ssePost).toHaveBeenCalled() }) - render(} />) - - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + const body = ssePost.mock.calls[0][1].body + expect(body.model_config.opening_statement).toBe('Hello!') + expect(body.model_config.suggested_questions).toEqual(['Q1']) }) - it('should handle opening feature disabled correctly', () => { - const { useFeatures } = require('@/app/components/base/features/hooks') + it('should omit opening statement when feature is disabled', async () => { + mockFeaturesState = { + ...defaultFeatures, + opening: { enabled: false, opening_statement: 'Should not appear', suggested_questions: ['Q1'] }, + } - useFeatures.mockReturnValue((selector: any) => { - const features = { - ...mockFeatures, - opening: { enabled: false, opening_statement: 'Should not appear', suggested_questions: ['Q1'] }, - } - return typeof selector === 'function' ? selector({ features }) : features + render(} />) + + fireEvent.click(screen.getByTestId('send-button')) + + const { ssePost } = require('@/service/base') as { ssePost: jest.Mock } + await waitFor(() => { + expect(ssePost).toHaveBeenCalled() }) - render(} />) - - // When opening is disabled, opening_statement should be empty - expect(screen.queryByText('Should not appear')).not.toBeInTheDocument() + const body = ssePost.mock.calls[0][1].body + expect(body.model_config.opening_statement).toBe('') + expect(body.model_config.suggested_questions).toEqual([]) }) it('should handle model without vision support', () => { @@ -689,7 +703,7 @@ describe('DebugWithSingleModel', () => { ], })) - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -710,7 +724,7 @@ describe('DebugWithSingleModel', () => { ], })) - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -735,7 +749,7 @@ describe('DebugWithSingleModel', () => { }), }) - render(} />) + render(} />) // Component should render successfully with filtered variables expect(screen.getByTestId('chat-component')).toBeInTheDocument() @@ -754,7 +768,7 @@ describe('DebugWithSingleModel', () => { }), }) - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -763,7 +777,7 @@ describe('DebugWithSingleModel', () => { // Tool Icons Tests describe('Tool Icons', () => { it('should map tool icons from collection list', () => { - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -783,7 +797,7 @@ describe('DebugWithSingleModel', () => { }), }) - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -812,7 +826,7 @@ describe('DebugWithSingleModel', () => { collectionList: [], }) - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -828,7 +842,7 @@ describe('DebugWithSingleModel', () => { inputs: {}, }) - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -846,7 +860,7 @@ describe('DebugWithSingleModel', () => { }, }) - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -859,7 +873,7 @@ describe('DebugWithSingleModel', () => { completionParams: {}, }) - render(} />) + render(} />) expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) @@ -868,7 +882,7 @@ describe('DebugWithSingleModel', () => { // Imperative Handle Tests describe('Imperative Handle', () => { it('should expose handleRestart method via ref', () => { - render(} />) + render(} />) expect(ref.current).not.toBeNull() expect(ref.current?.handleRestart).toBeDefined() @@ -876,65 +890,26 @@ describe('DebugWithSingleModel', () => { }) it('should call handleRestart when invoked via ref', () => { - render(} />) + render(} />) - expect(() => { + act(() => { ref.current?.handleRestart() - }).not.toThrow() - }) - }) - - // Memory and Performance Tests - describe('Memory and Performance', () => { - it('should properly memoize component', () => { - const { rerender } = render(} />) - - // Re-render with same props - rerender(} />) - - expect(screen.getByTestId('chat-component')).toBeInTheDocument() - }) - - it('should have displayName set for debugging', () => { - expect(DebugWithSingleModel).toBeDefined() - // memo wraps the component - expect(typeof DebugWithSingleModel).toBe('object') - }) - }) - - // Async Operations Tests - describe('Async Operations', () => { - it('should handle API calls during message send', async () => { - mockFetchConversationMessages.mockResolvedValue({ data: [] }) - - render(} />) - - const textarea = screen.getByRole('textbox', { hidden: true }) - fireEvent.change(textarea, { target: { value: 'Test message' } }) - - // Component should render without errors during async operations - await waitFor(() => { - expect(screen.getByTestId('chat-component')).toBeInTheDocument() - }) - }) - - it('should handle API errors gracefully', async () => { - mockFetchConversationMessages.mockRejectedValue(new Error('API Error')) - - render(} />) - - // Component should still render even if API calls fail - await waitFor(() => { - expect(screen.getByTestId('chat-component')).toBeInTheDocument() }) }) }) // File Upload Tests describe('File Upload', () => { - it('should not include files when vision is not supported', () => { + it('should not include files when vision is not supported', async () => { + const { useDebugConfigurationContext } = require('@/context/debug-configuration') const { useProviderContext } = require('@/context/provider-context') - const { useFeatures } = require('@/app/components/base/features/hooks') + + useDebugConfigurationContext.mockReturnValue({ + ...mockDebugConfigContext, + modelConfig: createMockModelConfig({ + model_id: 'gpt-3.5-turbo', + }), + }) useProviderContext.mockReturnValue(createMockProviderContext({ textGenerationModelList: [ @@ -961,23 +936,34 @@ describe('DebugWithSingleModel', () => { ], })) - useFeatures.mockReturnValue((selector: any) => { - const features = { - ...mockFeatures, - file: { enabled: true }, // File upload enabled - } - return typeof selector === 'function' ? selector({ features }) : features + mockFeaturesState = { + ...defaultFeatures, + file: { enabled: true }, + } + + render(} />) + + fireEvent.click(screen.getByTestId('send-with-files')) + + const { ssePost } = require('@/service/base') as { ssePost: jest.Mock } + await waitFor(() => { + expect(ssePost).toHaveBeenCalled() }) - render(} />) - - // Should render but not allow file uploads - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + const body = ssePost.mock.calls[0][1].body + expect(body.files).toEqual([]) }) - it('should support files when vision is enabled', () => { + it('should support files when vision is enabled', async () => { + const { useDebugConfigurationContext } = require('@/context/debug-configuration') const { useProviderContext } = require('@/context/provider-context') - const { useFeatures } = require('@/app/components/base/features/hooks') + + useDebugConfigurationContext.mockReturnValue({ + ...mockDebugConfigContext, + modelConfig: createMockModelConfig({ + model_id: 'gpt-4-vision', + }), + }) useProviderContext.mockReturnValue(createMockProviderContext({ textGenerationModelList: [ @@ -1004,17 +990,22 @@ describe('DebugWithSingleModel', () => { ], })) - useFeatures.mockReturnValue((selector: any) => { - const features = { - ...mockFeatures, - file: { enabled: true }, - } - return typeof selector === 'function' ? selector({ features }) : features + mockFeaturesState = { + ...defaultFeatures, + file: { enabled: true }, + } + + render(} />) + + fireEvent.click(screen.getByTestId('send-with-files')) + + const { ssePost } = require('@/service/base') as { ssePost: jest.Mock } + await waitFor(() => { + expect(ssePost).toHaveBeenCalled() }) - render(} />) - - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + const body = ssePost.mock.calls[0][1].body + expect(body.files).toHaveLength(1) }) }) }) diff --git a/web/app/components/billing/upgrade-btn/index.spec.tsx b/web/app/components/billing/upgrade-btn/index.spec.tsx index f52cc97b01..d106dbe327 100644 --- a/web/app/components/billing/upgrade-btn/index.spec.tsx +++ b/web/app/components/billing/upgrade-btn/index.spec.tsx @@ -5,24 +5,6 @@ import UpgradeBtn from './index' // ✅ Import real project components (DO NOT mock these) // PremiumBadge, Button, SparklesSoft are all base components -// ✅ Mock i18n with actual translations instead of returning keys -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string) => { - const translations: Record = { - 'billing.upgradeBtn.encourage': 'Upgrade to Pro', - 'billing.upgradeBtn.encourageShort': 'Upgrade', - 'billing.upgradeBtn.plain': 'Upgrade Plan', - 'custom.label.key': 'Custom Label', - 'custom.key': 'Custom Text', - 'custom.short.key': 'Short Custom', - 'custom.all': 'All Custom Props', - } - return translations[key] || key - }, - }), -})) - // ✅ Mock external dependencies only const mockSetShowPricingModal = jest.fn() jest.mock('@/context/modal-context', () => ({ @@ -52,7 +34,7 @@ describe('UpgradeBtn', () => { render() // Assert - should render with default text - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) it('should render premium badge by default', () => { @@ -60,7 +42,7 @@ describe('UpgradeBtn', () => { render() // Assert - PremiumBadge renders with text content - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) it('should render plain button when isPlain is true', () => { @@ -70,7 +52,7 @@ describe('UpgradeBtn', () => { // Assert - Button should be rendered with plain text const button = screen.getByRole('button') expect(button).toBeInTheDocument() - expect(screen.getByText(/upgrade plan/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.plain/i)).toBeInTheDocument() }) it('should render short text when isShort is true', () => { @@ -78,7 +60,7 @@ describe('UpgradeBtn', () => { render() // Assert - expect(screen.getByText(/^upgrade$/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourageShort/i)).toBeInTheDocument() }) it('should render custom label when labelKey is provided', () => { @@ -86,7 +68,7 @@ describe('UpgradeBtn', () => { render() // Assert - expect(screen.getByText(/custom label/i)).toBeInTheDocument() + expect(screen.getByText(/custom\.label\.key/i)).toBeInTheDocument() }) it('should render custom label in plain button when labelKey is provided with isPlain', () => { @@ -96,7 +78,7 @@ describe('UpgradeBtn', () => { // Assert const button = screen.getByRole('button') expect(button).toBeInTheDocument() - expect(screen.getByText(/custom label/i)).toBeInTheDocument() + expect(screen.getByText(/custom\.label\.key/i)).toBeInTheDocument() }) }) @@ -155,7 +137,7 @@ describe('UpgradeBtn', () => { render() // Assert - Component renders successfully with size prop - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) it('should render with size "m" by default', () => { @@ -163,7 +145,7 @@ describe('UpgradeBtn', () => { render() // Assert - Component renders successfully - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) it('should render with size "custom"', () => { @@ -171,7 +153,7 @@ describe('UpgradeBtn', () => { render() // Assert - Component renders successfully with custom size - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) }) @@ -184,8 +166,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert expect(handleClick).toHaveBeenCalledTimes(1) @@ -213,8 +195,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1) @@ -240,8 +222,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert expect(mockGtag).toHaveBeenCalledTimes(1) @@ -273,8 +255,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert expect(mockGtag).not.toHaveBeenCalled() @@ -287,8 +269,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert - should not throw error expect(mockGtag).not.toHaveBeenCalled() @@ -302,8 +284,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert expect(handleClick).toHaveBeenCalledTimes(1) @@ -321,7 +303,7 @@ describe('UpgradeBtn', () => { render() // Assert - should render without error - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) it('should handle undefined style', () => { @@ -329,7 +311,7 @@ describe('UpgradeBtn', () => { render() // Assert - should render without error - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) it('should handle undefined onClick', async () => { @@ -338,8 +320,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert - should fall back to setShowPricingModal expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1) @@ -351,8 +333,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert - should not attempt to track gtag expect(mockGtag).not.toHaveBeenCalled() @@ -363,7 +345,7 @@ describe('UpgradeBtn', () => { render() // Assert - should use default label - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) it('should handle empty string className', () => { @@ -371,7 +353,7 @@ describe('UpgradeBtn', () => { render() // Assert - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) it('should handle empty string loc', async () => { @@ -380,8 +362,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert - empty loc should not trigger gtag expect(mockGtag).not.toHaveBeenCalled() @@ -392,7 +374,7 @@ describe('UpgradeBtn', () => { render() // Assert - empty labelKey is falsy, so it falls back to default label - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.encourage/i)).toBeInTheDocument() }) }) @@ -403,7 +385,7 @@ describe('UpgradeBtn', () => { render() // Assert - isShort should not affect plain button text - expect(screen.getByText(/upgrade plan/i)).toBeInTheDocument() + expect(screen.getByText(/billing\.upgradeBtn\.plain/i)).toBeInTheDocument() }) it('should handle isPlain with custom labelKey', () => { @@ -411,8 +393,8 @@ describe('UpgradeBtn', () => { render() // Assert - labelKey should override plain text - expect(screen.getByText(/custom text/i)).toBeInTheDocument() - expect(screen.queryByText(/upgrade plan/i)).not.toBeInTheDocument() + expect(screen.getByText(/custom\.key/i)).toBeInTheDocument() + expect(screen.queryByText(/billing\.upgradeBtn\.plain/i)).not.toBeInTheDocument() }) it('should handle isShort with custom labelKey', () => { @@ -420,8 +402,8 @@ describe('UpgradeBtn', () => { render() // Assert - labelKey should override isShort behavior - expect(screen.getByText(/short custom/i)).toBeInTheDocument() - expect(screen.queryByText(/^upgrade$/i)).not.toBeInTheDocument() + expect(screen.getByText(/custom\.short\.key/i)).toBeInTheDocument() + expect(screen.queryByText(/billing\.upgradeBtn\.encourageShort/i)).not.toBeInTheDocument() }) it('should handle all custom props together', async () => { @@ -443,14 +425,14 @@ describe('UpgradeBtn', () => { labelKey="custom.all" />, ) - const badge = screen.getByText(/all custom props/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/custom\.all/i) + await user.click(badge) // Assert const rootElement = container.firstChild as HTMLElement expect(rootElement).toHaveClass(customClass) expect(rootElement).toHaveStyle(customStyle) - expect(screen.getByText(/all custom props/i)).toBeInTheDocument() + expect(screen.getByText(/custom\.all/i)).toBeInTheDocument() expect(handleClick).toHaveBeenCalledTimes(1) expect(mockGtag).toHaveBeenCalledWith('event', 'click_upgrade_btn', { loc: 'test-loc', @@ -503,10 +485,10 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) // Click badge - await user.click(badge!) + await user.click(badge) // Assert expect(handleClick).toHaveBeenCalledTimes(1) @@ -522,70 +504,6 @@ describe('UpgradeBtn', () => { }) }) - // Performance Tests - describe('Performance', () => { - it('should not rerender when props do not change', () => { - // Arrange - const { rerender } = render() - const firstRender = screen.getByText(/upgrade to pro/i) - - // Act - Rerender with same props - rerender() - - // Assert - Component should still be in document - expect(firstRender).toBeInTheDocument() - expect(screen.getByText(/upgrade to pro/i)).toBe(firstRender) - }) - - it('should rerender when props change', () => { - // Arrange - const { rerender } = render() - expect(screen.getByText(/custom text/i)).toBeInTheDocument() - - // Act - Rerender with different labelKey - rerender() - - // Assert - Should show new label - expect(screen.getByText(/custom label/i)).toBeInTheDocument() - expect(screen.queryByText(/custom text/i)).not.toBeInTheDocument() - }) - - it('should handle rapid rerenders efficiently', () => { - // Arrange - const { rerender } = render() - - // Act - Multiple rapid rerenders - for (let i = 0; i < 10; i++) - rerender() - - // Assert - Component should still render correctly - expect(screen.getByText(/upgrade to pro/i)).toBeInTheDocument() - }) - - it('should be memoized with React.memo', () => { - // Arrange - const TestWrapper = ({ children }: { children: React.ReactNode }) =>
{children}
- - const { rerender } = render( - - - , - ) - - const firstElement = screen.getByText(/upgrade to pro/i) - - // Act - Rerender parent with same props - rerender( - - - , - ) - - // Assert - Element reference should be stable due to memo - expect(screen.getByText(/upgrade to pro/i)).toBe(firstElement) - }) - }) - // Integration Tests describe('Integration', () => { it('should work with modal context for pricing modal', async () => { @@ -594,8 +512,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert await waitFor(() => { @@ -610,8 +528,8 @@ describe('UpgradeBtn', () => { // Act render() - const badge = screen.getByText(/upgrade to pro/i).closest('div') - await user.click(badge!) + const badge = screen.getByText(/billing\.upgradeBtn\.encourage/i) + await user.click(badge) // Assert - Both onClick and gtag should be called await waitFor(() => { diff --git a/web/app/components/explore/installed-app/index.spec.tsx b/web/app/components/explore/installed-app/index.spec.tsx index 61ef575183..7dbf31aa42 100644 --- a/web/app/components/explore/installed-app/index.spec.tsx +++ b/web/app/components/explore/installed-app/index.spec.tsx @@ -172,7 +172,7 @@ describe('InstalledApp', () => { describe('Rendering', () => { it('should render without crashing', () => { render() - expect(screen.getByTestId('chat-with-history')).toBeInTheDocument() + expect(screen.getByText(/Chat With History/i)).toBeInTheDocument() }) it('should render loading state when fetching app params', () => { @@ -296,8 +296,8 @@ describe('InstalledApp', () => { describe('App Mode Rendering', () => { it('should render ChatWithHistory for CHAT mode', () => { render() - expect(screen.getByTestId('chat-with-history')).toBeInTheDocument() - expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument() + expect(screen.getByText(/Chat With History/i)).toBeInTheDocument() + expect(screen.queryByText(/Text Generation App/i)).not.toBeInTheDocument() }) it('should render ChatWithHistory for ADVANCED_CHAT mode', () => { @@ -314,8 +314,8 @@ describe('InstalledApp', () => { }) render() - expect(screen.getByTestId('chat-with-history')).toBeInTheDocument() - expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument() + expect(screen.getByText(/Chat With History/i)).toBeInTheDocument() + expect(screen.queryByText(/Text Generation App/i)).not.toBeInTheDocument() }) it('should render ChatWithHistory for AGENT_CHAT mode', () => { @@ -332,8 +332,8 @@ describe('InstalledApp', () => { }) render() - expect(screen.getByTestId('chat-with-history')).toBeInTheDocument() - expect(screen.queryByTestId('text-generation-app')).not.toBeInTheDocument() + expect(screen.getByText(/Chat With History/i)).toBeInTheDocument() + expect(screen.queryByText(/Text Generation App/i)).not.toBeInTheDocument() }) it('should render TextGenerationApp for COMPLETION mode', () => { @@ -350,8 +350,7 @@ describe('InstalledApp', () => { }) render() - expect(screen.getByTestId('text-generation-app')).toBeInTheDocument() - expect(screen.getByText(/Text Generation App/)).toBeInTheDocument() + expect(screen.getByText(/Text Generation App/i)).toBeInTheDocument() expect(screen.queryByText(/Workflow/)).not.toBeInTheDocument() }) @@ -369,7 +368,7 @@ describe('InstalledApp', () => { }) render() - expect(screen.getByTestId('text-generation-app')).toBeInTheDocument() + expect(screen.getByText(/Text Generation App/i)).toBeInTheDocument() expect(screen.getByText(/Workflow/)).toBeInTheDocument() }) }) @@ -566,22 +565,10 @@ describe('InstalledApp', () => { render() // Should find and render the correct app - expect(screen.getByTestId('chat-with-history')).toBeInTheDocument() + expect(screen.getByText(/Chat With History/i)).toBeInTheDocument() expect(screen.getByText(/installed-app-123/)).toBeInTheDocument() }) - it('should apply correct CSS classes to container', () => { - const { container } = render() - const mainDiv = container.firstChild as HTMLElement - expect(mainDiv).toHaveClass('h-full', 'bg-background-default', 'py-2', 'pl-0', 'pr-2', 'sm:p-2') - }) - - it('should apply correct CSS classes to ChatWithHistory', () => { - render() - const chatComponent = screen.getByTestId('chat-with-history') - expect(chatComponent).toHaveClass('overflow-hidden', 'rounded-2xl', 'shadow-md') - }) - it('should handle rapid id prop changes', async () => { const app1 = { ...mockInstalledApp, id: 'app-1' } const app2 = { ...mockInstalledApp, id: 'app-2' } @@ -627,50 +614,6 @@ describe('InstalledApp', () => { }) }) - describe('Component Memoization', () => { - it('should be wrapped with React.memo', () => { - // React.memo wraps the component with a special $$typeof symbol - const componentType = (InstalledApp as React.MemoExoticComponent).$$typeof - expect(componentType).toBeDefined() - }) - - it('should re-render when props change', () => { - const { rerender } = render() - expect(screen.getByText(/installed-app-123/)).toBeInTheDocument() - - // Change to a different app - const differentApp = { - ...mockInstalledApp, - id: 'different-app-456', - app: { - ...mockInstalledApp.app, - name: 'Different App', - }, - } - ;(useContext as jest.Mock).mockReturnValue({ - installedApps: [differentApp], - isFetchingInstalledApps: false, - }) - - rerender() - expect(screen.getByText(/different-app-456/)).toBeInTheDocument() - }) - - it('should maintain component stability across re-renders with same props', () => { - const { rerender } = render() - const initialCallCount = mockUpdateAppInfo.mock.calls.length - - // Rerender with same props - useEffect may still run due to dependencies - rerender() - - // Component should render successfully - expect(screen.getByTestId('chat-with-history')).toBeInTheDocument() - - // Mock calls might increase due to useEffect, but component should be stable - expect(mockUpdateAppInfo.mock.calls.length).toBeGreaterThanOrEqual(initialCallCount) - }) - }) - describe('Render Priority', () => { it('should show error before loading state', () => { ;(useGetInstalledAppParams as jest.Mock).mockReturnValue({ diff --git a/web/jest.config.ts b/web/jest.config.ts index 6c2d88448c..e86ec5af74 100644 --- a/web/jest.config.ts +++ b/web/jest.config.ts @@ -44,6 +44,7 @@ const config: Config = { // A list of reporter names that Jest uses when writing coverage reports coverageReporters: [ + 'json-summary', 'json', 'text', 'text-summary', diff --git a/web/jest.setup.ts b/web/jest.setup.ts index 9c3b0bf3bd..a4d358d805 100644 --- a/web/jest.setup.ts +++ b/web/jest.setup.ts @@ -42,6 +42,22 @@ if (typeof window !== 'undefined') { ensureWritable(HTMLElement.prototype, 'focus') } +if (typeof globalThis.ResizeObserver === 'undefined') { + globalThis.ResizeObserver = class { + observe() { + return undefined + } + + unobserve() { + return undefined + } + + disconnect() { + return undefined + } + } +} + afterEach(() => { cleanup() }) diff --git a/web/package.json b/web/package.json index d54e6effb2..158dfbcae8 100644 --- a/web/package.json +++ b/web/package.json @@ -200,6 +200,7 @@ "eslint-plugin-tailwindcss": "^3.18.2", "globals": "^15.15.0", "husky": "^9.1.7", + "istanbul-lib-coverage": "^3.2.2", "jest": "^29.7.0", "jsdom-testing-mocks": "^1.16.0", "knip": "^5.66.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 8523215a07..6dbc0fabd9 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -512,6 +512,9 @@ importers: husky: specifier: ^9.1.7 version: 9.1.7 + istanbul-lib-coverage: + specifier: ^3.2.2 + version: 3.2.2 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@18.15.0)(ts-node@10.9.2(@types/node@18.15.0)(typescript@5.9.3)) From fbbff7f5c253aafc545eb5c9657f3cd6bedcd1c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:49:48 +0800 Subject: [PATCH 05/17] chore(deps-dev): bump storybook from 9.1.13 to 9.1.17 in /web (#29906) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- web/package.json | 2 +- web/pnpm-lock.yaml | 102 ++++++++++++++++++++++----------------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/web/package.json b/web/package.json index 158dfbcae8..541a12450a 100644 --- a/web/package.json +++ b/web/package.json @@ -212,7 +212,7 @@ "postcss": "^8.5.6", "react-scan": "^0.4.3", "sass": "^1.93.2", - "storybook": "9.1.13", + "storybook": "9.1.17", "tailwindcss": "^3.4.18", "ts-node": "^10.9.2", "typescript": "^5.9.3", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 6dbc0fabd9..b75839d046 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -364,7 +364,7 @@ importers: version: 7.28.5 '@chromatic-com/storybook': specifier: ^4.1.1 - version: 4.1.3(storybook@9.1.13(@testing-library/dom@10.4.1)) + version: 4.1.3(storybook@9.1.17(@testing-library/dom@10.4.1)) '@eslint-react/eslint-plugin': specifier: ^1.53.1 version: 1.53.1(eslint@9.39.1(jiti@1.21.7))(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3) @@ -391,22 +391,22 @@ importers: version: 4.2.0 '@storybook/addon-docs': specifier: 9.1.13 - version: 9.1.13(@types/react@19.2.7)(storybook@9.1.13(@testing-library/dom@10.4.1)) + version: 9.1.13(@types/react@19.2.7)(storybook@9.1.17(@testing-library/dom@10.4.1)) '@storybook/addon-links': specifier: 9.1.13 - version: 9.1.13(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1)) + version: 9.1.13(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)) '@storybook/addon-onboarding': specifier: 9.1.13 - version: 9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)) + version: 9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1)) '@storybook/addon-themes': specifier: 9.1.13 - version: 9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)) + version: 9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1)) '@storybook/nextjs': specifier: 9.1.13 - version: 9.1.13(esbuild@0.25.0)(next@15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0)(storybook@9.1.13(@testing-library/dom@10.4.1))(type-fest@4.2.0)(typescript@5.9.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) + version: 9.1.13(esbuild@0.25.0)(next@15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0)(storybook@9.1.17(@testing-library/dom@10.4.1))(type-fest@4.2.0)(typescript@5.9.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) '@storybook/react': specifier: 9.1.13 - version: 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3) + version: 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -502,7 +502,7 @@ importers: version: 3.0.5(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-storybook: specifier: ^9.1.13 - version: 9.1.16(eslint@9.39.1(jiti@1.21.7))(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3) + version: 9.1.16(eslint@9.39.1(jiti@1.21.7))(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3) eslint-plugin-tailwindcss: specifier: ^3.18.2 version: 3.18.2(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.2)) @@ -549,8 +549,8 @@ importers: specifier: ^1.93.2 version: 1.95.0 storybook: - specifier: 9.1.13 - version: 9.1.13(@testing-library/dom@10.4.1) + specifier: 9.1.17 + version: 9.1.17(@testing-library/dom@10.4.1) tailwindcss: specifier: ^3.4.18 version: 3.4.18(tsx@4.21.0)(yaml@2.8.2) @@ -6447,8 +6447,8 @@ packages: lexical@0.38.2: resolution: {integrity: sha512-JJmfsG3c4gwBHzUGffbV7ifMNkKAWMCnYE3xJl87gty7hjyV5f3xq7eqTjP5HFYvO4XpjJvvWO2/djHp5S10tw==} - lib0@0.2.114: - resolution: {integrity: sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==} + lib0@0.2.115: + resolution: {integrity: sha512-noaW4yNp6hCjOgDnWWxW0vGXE3kZQI5Kqiwz+jIWXavI9J9WyfJ9zjsbQlQlgjIbHBrvlA/x3TSIXBUJj+0L6g==} engines: {node: '>=16'} hasBin: true @@ -8090,8 +8090,8 @@ packages: state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} - storybook@9.1.13: - resolution: {integrity: sha512-G3KZ36EVzXyHds72B/qtWiJnhUpM0xOUeYlDcO9DSHL1bDTv15cW4+upBl+mcBZrDvU838cn7Bv4GpF+O5MCfw==} + storybook@9.1.17: + resolution: {integrity: sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -10012,13 +10012,13 @@ snapshots: '@chevrotain/utils@11.0.3': {} - '@chromatic-com/storybook@4.1.3(storybook@9.1.13(@testing-library/dom@10.4.1))': + '@chromatic-com/storybook@4.1.3(storybook@9.1.17(@testing-library/dom@10.4.1))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 13.3.4 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) strip-ansi: 7.1.2 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -11888,38 +11888,38 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@storybook/addon-docs@9.1.13(@types/react@19.2.7)(storybook@9.1.13(@testing-library/dom@10.4.1))': + '@storybook/addon-docs@9.1.13(@types/react@19.2.7)(storybook@9.1.17(@testing-library/dom@10.4.1))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.3) - '@storybook/csf-plugin': 9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)) + '@storybook/csf-plugin': 9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1)) '@storybook/icons': 1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@storybook/react-dom-shim': 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1)) + '@storybook/react-dom-shim': 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-links@9.1.13(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1))': + '@storybook/addon-links@9.1.13(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) optionalDependencies: react: 19.2.3 - '@storybook/addon-onboarding@9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1))': + '@storybook/addon-onboarding@9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1))': dependencies: - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) - '@storybook/addon-themes@9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1))': + '@storybook/addon-themes@9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1))': dependencies: - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) ts-dedent: 2.2.0 - '@storybook/builder-webpack5@9.1.13(esbuild@0.25.0)(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3)(uglify-js@3.19.3)': + '@storybook/builder-webpack5@9.1.13(esbuild@0.25.0)(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3)(uglify-js@3.19.3)': dependencies: - '@storybook/core-webpack': 9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)) + '@storybook/core-webpack': 9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1)) case-sensitive-paths-webpack-plugin: 2.4.0 cjs-module-lexer: 1.4.3 css-loader: 6.11.0(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) @@ -11927,7 +11927,7 @@ snapshots: fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.9.3)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) html-webpack-plugin: 5.6.5(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) magic-string: 0.30.21 - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) style-loader: 3.3.4(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) terser-webpack-plugin: 5.3.15(esbuild@0.25.0)(uglify-js@3.19.3)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) ts-dedent: 2.2.0 @@ -11944,14 +11944,14 @@ snapshots: - uglify-js - webpack-cli - '@storybook/core-webpack@9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1))': + '@storybook/core-webpack@9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1))': dependencies: - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) ts-dedent: 2.2.0 - '@storybook/csf-plugin@9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1))': + '@storybook/csf-plugin@9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1))': dependencies: - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -11961,7 +11961,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@storybook/nextjs@9.1.13(esbuild@0.25.0)(next@15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0)(storybook@9.1.13(@testing-library/dom@10.4.1))(type-fest@4.2.0)(typescript@5.9.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3))': + '@storybook/nextjs@9.1.13(esbuild@0.25.0)(next@15.5.9(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.95.0)(storybook@9.1.17(@testing-library/dom@10.4.1))(type-fest@4.2.0)(typescript@5.9.3)(uglify-js@3.19.3)(webpack-hot-middleware@2.26.1)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) @@ -11977,9 +11977,9 @@ snapshots: '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) '@babel/runtime': 7.28.4 '@pmmmwh/react-refresh-webpack-plugin': 0.5.17(react-refresh@0.14.2)(type-fest@4.2.0)(webpack-hot-middleware@2.26.1)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) - '@storybook/builder-webpack5': 9.1.13(esbuild@0.25.0)(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3)(uglify-js@3.19.3) - '@storybook/preset-react-webpack': 9.1.13(esbuild@0.25.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3)(uglify-js@3.19.3) - '@storybook/react': 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3) + '@storybook/builder-webpack5': 9.1.13(esbuild@0.25.0)(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3)(uglify-js@3.19.3) + '@storybook/preset-react-webpack': 9.1.13(esbuild@0.25.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3)(uglify-js@3.19.3) + '@storybook/react': 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3) '@types/semver': 7.7.1 babel-loader: 9.2.1(@babel/core@7.28.5)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) css-loader: 6.11.0(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) @@ -11995,7 +11995,7 @@ snapshots: resolve-url-loader: 5.0.0 sass-loader: 16.0.6(sass@1.95.0)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) semver: 7.7.3 - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) style-loader: 3.3.4(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) styled-jsx: 5.1.7(@babel/core@7.28.5)(react@19.2.3) tsconfig-paths: 4.2.0 @@ -12021,9 +12021,9 @@ snapshots: - webpack-hot-middleware - webpack-plugin-serve - '@storybook/preset-react-webpack@9.1.13(esbuild@0.25.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3)(uglify-js@3.19.3)': + '@storybook/preset-react-webpack@9.1.13(esbuild@0.25.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3)(uglify-js@3.19.3)': dependencies: - '@storybook/core-webpack': 9.1.13(storybook@9.1.13(@testing-library/dom@10.4.1)) + '@storybook/core-webpack': 9.1.13(storybook@9.1.17(@testing-library/dom@10.4.1)) '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.9.3)(webpack@5.103.0(esbuild@0.25.0)(uglify-js@3.19.3)) '@types/semver': 7.7.1 find-up: 7.0.0 @@ -12033,7 +12033,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) resolve: 1.22.11 semver: 7.7.3 - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) tsconfig-paths: 4.2.0 webpack: 5.103.0(esbuild@0.25.0)(uglify-js@3.19.3) optionalDependencies: @@ -12059,19 +12059,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/react-dom-shim@9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1))': + '@storybook/react-dom-shim@9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1))': dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) - '@storybook/react@9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3)': + '@storybook/react@9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.13(@testing-library/dom@10.4.1)) + '@storybook/react-dom-shim': 9.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@9.1.17(@testing-library/dom@10.4.1)) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) optionalDependencies: typescript: 5.9.3 @@ -14380,11 +14380,11 @@ snapshots: semver: 7.7.2 typescript: 5.9.3 - eslint-plugin-storybook@9.1.16(eslint@9.39.1(jiti@1.21.7))(storybook@9.1.13(@testing-library/dom@10.4.1))(typescript@5.9.3): + eslint-plugin-storybook@9.1.16(eslint@9.39.1(jiti@1.21.7))(storybook@9.1.17(@testing-library/dom@10.4.1))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) eslint: 9.39.1(jiti@1.21.7) - storybook: 9.1.13(@testing-library/dom@10.4.1) + storybook: 9.1.17(@testing-library/dom@10.4.1) transitivePeerDependencies: - supports-color - typescript @@ -15811,7 +15811,7 @@ snapshots: lexical@0.38.2: {} - lib0@0.2.114: + lib0@0.2.115: dependencies: isomorphic.js: 0.2.5 @@ -17944,7 +17944,7 @@ snapshots: state-local@1.0.7: {} - storybook@9.1.13(@testing-library/dom@10.4.1): + storybook@9.1.17(@testing-library/dom@10.4.1): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.9.1 @@ -18838,7 +18838,7 @@ snapshots: yjs@13.6.27: dependencies: - lib0: 0.2.114 + lib0: 0.2.115 yn@3.1.1: {} From 95a2b3d08837b018317c7e62de1ec0c31d01ccb3 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Fri, 19 Dec 2025 13:00:34 +0900 Subject: [PATCH 06/17] refactor: split changes for api/libs/helper.py (#29875) --- api/libs/helper.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/api/libs/helper.py b/api/libs/helper.py index 4a7afe0bda..74e1808e49 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -11,6 +11,7 @@ from collections.abc import Generator, Mapping from datetime import datetime from hashlib import sha256 from typing import TYPE_CHECKING, Annotated, Any, Optional, Union, cast +from uuid import UUID from zoneinfo import available_timezones from flask import Response, stream_with_context @@ -119,6 +120,19 @@ def uuid_value(value: Any) -> str: raise ValueError(error) +def normalize_uuid(value: str | UUID) -> str: + if not value: + return "" + + try: + return uuid_value(value) + except ValueError as exc: + raise ValueError("must be a valid UUID") from exc + + +UUIDStrOrEmpty = Annotated[str, AfterValidator(normalize_uuid)] + + def alphanumeric(value: str): # check if the value is alphanumeric and underlined if re.match(r"^[a-zA-Z0-9_]+$", value): From 80f11471aeef1885246cf1ef157b495b7c9c9c9b Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:00:46 +0800 Subject: [PATCH 07/17] perf: improve Jest caching and configuration in web tests (#29881) --- .github/workflows/web-tests.yml | 10 +- .../edit-item/index.spec.tsx | 73 ++++++- .../edit-annotation-modal/edit-item/index.tsx | 23 ++- .../edit-annotation-modal/index.spec.tsx | 192 ++++++++++++++---- .../edit-annotation-modal/index.tsx | 52 +++-- .../app/annotation/header-opts/index.spec.tsx | 116 +++++++++++ web/app/components/app/annotation/type.ts | 6 + .../params-config/index.spec.tsx | 103 +++++++--- .../annotation-ctrl-button.tsx | 4 +- web/i18n/ar-TN/common.ts | 1 + web/i18n/de-DE/common.ts | 1 + web/i18n/en-US/common.ts | 1 + web/i18n/es-ES/common.ts | 1 + web/i18n/fa-IR/common.ts | 1 + web/i18n/fr-FR/common.ts | 1 + web/i18n/hi-IN/common.ts | 1 + web/i18n/id-ID/common.ts | 1 + web/i18n/it-IT/common.ts | 1 + web/i18n/ja-JP/common.ts | 1 + web/i18n/ko-KR/common.ts | 1 + web/i18n/pl-PL/common.ts | 1 + web/i18n/pt-BR/common.ts | 1 + web/i18n/ro-RO/common.ts | 1 + web/i18n/ru-RU/common.ts | 1 + web/i18n/sl-SI/common.ts | 1 + web/i18n/th-TH/common.ts | 1 + web/i18n/tr-TR/common.ts | 1 + web/i18n/uk-UA/common.ts | 1 + web/i18n/vi-VN/common.ts | 1 + web/i18n/zh-Hans/common.ts | 1 + web/i18n/zh-Hant/common.ts | 1 + web/jest.config.ts | 2 +- web/service/annotation.ts | 4 +- 33 files changed, 496 insertions(+), 111 deletions(-) diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 8b871403cc..b1f32f96c2 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -35,6 +35,14 @@ jobs: cache: pnpm cache-dependency-path: ./web/pnpm-lock.yaml + - name: Restore Jest cache + uses: actions/cache@v4 + with: + path: web/.cache/jest + key: ${{ runner.os }}-jest-${{ hashFiles('web/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-jest- + - name: Install dependencies run: pnpm install --frozen-lockfile @@ -45,7 +53,7 @@ jobs: run: | pnpm exec jest \ --ci \ - --runInBand \ + --maxWorkers=100% \ --coverage \ --passWithNoTests diff --git a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.spec.tsx index 1f32e55928..95a5586292 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.spec.tsx @@ -245,7 +245,7 @@ describe('EditItem', () => { expect(mockSave).toHaveBeenCalledWith('Test save content') }) - it('should show delete option when content changes', async () => { + it('should show delete option and restore original content when delete is clicked', async () => { // Arrange const mockSave = jest.fn().mockResolvedValue(undefined) const props = { @@ -267,7 +267,13 @@ describe('EditItem', () => { await user.click(screen.getByRole('button', { name: 'common.operation.save' })) // Assert - expect(mockSave).toHaveBeenCalledWith('Modified content') + expect(mockSave).toHaveBeenNthCalledWith(1, 'Modified content') + expect(await screen.findByText('common.operation.delete')).toBeInTheDocument() + + await user.click(screen.getByText('common.operation.delete')) + + expect(mockSave).toHaveBeenNthCalledWith(2, 'Test content') + expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument() }) it('should handle keyboard interactions in edit mode', async () => { @@ -393,5 +399,68 @@ describe('EditItem', () => { expect(screen.queryByRole('textbox')).not.toBeInTheDocument() expect(screen.getByText('Test content')).toBeInTheDocument() }) + + it('should handle save failure gracefully in edit mode', async () => { + // Arrange + const mockSave = jest.fn().mockRejectedValueOnce(new Error('Save failed')) + const props = { + ...defaultProps, + onSave: mockSave, + } + const user = userEvent.setup() + + // Act + render() + + // Enter edit mode and save (should fail) + await user.click(screen.getByText('common.operation.edit')) + const textarea = screen.getByRole('textbox') + await user.type(textarea, 'New content') + + // Save should fail but not throw + await user.click(screen.getByRole('button', { name: 'common.operation.save' })) + + // Assert - Should remain in edit mode when save fails + expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() + expect(mockSave).toHaveBeenCalledWith('New content') + }) + + it('should handle delete action failure gracefully', async () => { + // Arrange + const mockSave = jest.fn() + .mockResolvedValueOnce(undefined) // First save succeeds + .mockRejectedValueOnce(new Error('Delete failed')) // Delete fails + const props = { + ...defaultProps, + onSave: mockSave, + } + const user = userEvent.setup() + + // Act + render() + + // Edit content to show delete button + await user.click(screen.getByText('common.operation.edit')) + const textarea = screen.getByRole('textbox') + await user.clear(textarea) + await user.type(textarea, 'Modified content') + + // Save to create new content + await user.click(screen.getByRole('button', { name: 'common.operation.save' })) + await screen.findByText('common.operation.delete') + + // Click delete (should fail but not throw) + await user.click(screen.getByText('common.operation.delete')) + + // Assert - Delete action should handle error gracefully + expect(mockSave).toHaveBeenCalledTimes(2) + expect(mockSave).toHaveBeenNthCalledWith(1, 'Modified content') + expect(mockSave).toHaveBeenNthCalledWith(2, 'Test content') + + // When delete fails, the delete button should still be visible (state not changed) + expect(screen.getByText('common.operation.delete')).toBeInTheDocument() + expect(screen.getByText('Modified content')).toBeInTheDocument() + }) }) }) diff --git a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx index e808d0b48a..37b5ab0686 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx @@ -52,8 +52,14 @@ const EditItem: FC = ({ }, [content]) const handleSave = async () => { - await onSave(newContent) - setIsEdit(false) + try { + await onSave(newContent) + setIsEdit(false) + } + catch { + // Keep edit mode open when save fails + // Error notification is handled by the parent component + } } const handleCancel = () => { @@ -96,9 +102,16 @@ const EditItem: FC = ({
·
{ - setNewContent(content) - onSave(content) + onClick={async () => { + try { + await onSave(content) + // Only update UI state after successful delete + setNewContent(content) + } + catch { + // Delete action failed - error is already handled by parent + // UI state remains unchanged, user can retry + } }} >
diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx index b48f8a2a4a..bdc991116c 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import Toast, { type IToastProps, type ToastHandle } from '@/app/components/base/toast' import EditAnnotationModal from './index' @@ -408,7 +408,7 @@ describe('EditAnnotationModal', () => { // Error Handling (CRITICAL for coverage) describe('Error Handling', () => { - it('should handle addAnnotation API failure gracefully', async () => { + it('should show error toast and skip callbacks when addAnnotation fails', async () => { // Arrange const mockOnAdded = jest.fn() const props = { @@ -420,29 +420,75 @@ describe('EditAnnotationModal', () => { // Mock API failure mockAddAnnotation.mockRejectedValueOnce(new Error('API Error')) - // Act & Assert - Should handle API error without crashing - expect(async () => { - render() + // Act + render() - // Find and click edit link for query - const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + // Find and click edit link for query + const editLinks = screen.getAllByText(/common\.operation\.edit/i) + await user.click(editLinks[0]) - // Find textarea and enter new content - const textarea = screen.getByRole('textbox') - await user.clear(textarea) - await user.type(textarea, 'New query content') + // Find textarea and enter new content + const textarea = screen.getByRole('textbox') + await user.clear(textarea) + await user.type(textarea, 'New query content') - // Click save button - const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) - await user.click(saveButton) + // Click save button + const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) + await user.click(saveButton) - // Should not call onAdded on error - expect(mockOnAdded).not.toHaveBeenCalled() - }).not.toThrow() + // Assert + await waitFor(() => { + expect(toastNotifySpy).toHaveBeenCalledWith({ + message: 'API Error', + type: 'error', + }) + }) + expect(mockOnAdded).not.toHaveBeenCalled() + + // Verify edit mode remains open (textarea should still be visible) + expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() }) - it('should handle editAnnotation API failure gracefully', async () => { + it('should show fallback error message when addAnnotation error has no message', async () => { + // Arrange + const mockOnAdded = jest.fn() + const props = { + ...defaultProps, + onAdded: mockOnAdded, + } + const user = userEvent.setup() + + mockAddAnnotation.mockRejectedValueOnce({}) + + // Act + render() + + const editLinks = screen.getAllByText(/common\.operation\.edit/i) + await user.click(editLinks[0]) + + const textarea = screen.getByRole('textbox') + await user.clear(textarea) + await user.type(textarea, 'New query content') + + const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) + await user.click(saveButton) + + // Assert + await waitFor(() => { + expect(toastNotifySpy).toHaveBeenCalledWith({ + message: 'common.api.actionFailed', + type: 'error', + }) + }) + expect(mockOnAdded).not.toHaveBeenCalled() + + // Verify edit mode remains open (textarea should still be visible) + expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() + }) + + it('should show error toast and skip callbacks when editAnnotation fails', async () => { // Arrange const mockOnEdited = jest.fn() const props = { @@ -456,24 +502,72 @@ describe('EditAnnotationModal', () => { // Mock API failure mockEditAnnotation.mockRejectedValueOnce(new Error('API Error')) - // Act & Assert - Should handle API error without crashing - expect(async () => { - render() + // Act + render() - // Edit query content - const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + // Edit query content + const editLinks = screen.getAllByText(/common\.operation\.edit/i) + await user.click(editLinks[0]) - const textarea = screen.getByRole('textbox') - await user.clear(textarea) - await user.type(textarea, 'Modified query') + const textarea = screen.getByRole('textbox') + await user.clear(textarea) + await user.type(textarea, 'Modified query') - const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) - await user.click(saveButton) + const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) + await user.click(saveButton) - // Should not call onEdited on error - expect(mockOnEdited).not.toHaveBeenCalled() - }).not.toThrow() + // Assert + await waitFor(() => { + expect(toastNotifySpy).toHaveBeenCalledWith({ + message: 'API Error', + type: 'error', + }) + }) + expect(mockOnEdited).not.toHaveBeenCalled() + + // Verify edit mode remains open (textarea should still be visible) + expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() + }) + + it('should show fallback error message when editAnnotation error is not an Error instance', async () => { + // Arrange + const mockOnEdited = jest.fn() + const props = { + ...defaultProps, + annotationId: 'test-annotation-id', + messageId: 'test-message-id', + onEdited: mockOnEdited, + } + const user = userEvent.setup() + + mockEditAnnotation.mockRejectedValueOnce('oops') + + // Act + render() + + const editLinks = screen.getAllByText(/common\.operation\.edit/i) + await user.click(editLinks[0]) + + const textarea = screen.getByRole('textbox') + await user.clear(textarea) + await user.type(textarea, 'Modified query') + + const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) + await user.click(saveButton) + + // Assert + await waitFor(() => { + expect(toastNotifySpy).toHaveBeenCalledWith({ + message: 'common.api.actionFailed', + type: 'error', + }) + }) + expect(mockOnEdited).not.toHaveBeenCalled() + + // Verify edit mode remains open (textarea should still be visible) + expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() }) }) @@ -526,25 +620,33 @@ describe('EditAnnotationModal', () => { }) }) - // Toast Notifications (Simplified) + // Toast Notifications (Success) describe('Toast Notifications', () => { - it('should trigger success notification when save operation completes', async () => { + it('should show success notification when save operation completes', async () => { // Arrange - const mockOnAdded = jest.fn() - const props = { - ...defaultProps, - onAdded: mockOnAdded, - } + const props = { ...defaultProps } + const user = userEvent.setup() // Act render() - // Simulate successful save by calling handleSave indirectly - const mockSave = jest.fn() - expect(mockSave).not.toHaveBeenCalled() + const editLinks = screen.getAllByText(/common\.operation\.edit/i) + await user.click(editLinks[0]) - // Assert - Toast spy is available and will be called during real save operations - expect(toastNotifySpy).toBeDefined() + const textarea = screen.getByRole('textbox') + await user.clear(textarea) + await user.type(textarea, 'Updated query') + + const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) + await user.click(saveButton) + + // Assert + await waitFor(() => { + expect(toastNotifySpy).toHaveBeenCalledWith({ + message: 'common.api.actionSuccess', + type: 'success', + }) + }) }) }) diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index 2961ce393c..6172a215e4 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -53,27 +53,39 @@ const EditAnnotationModal: FC = ({ postQuery = editedContent else postAnswer = editedContent - if (!isAdd) { - await editAnnotation(appId, annotationId, { - message_id: messageId, - question: postQuery, - answer: postAnswer, - }) - onEdited(postQuery, postAnswer) - } - else { - const res: any = await addAnnotation(appId, { - question: postQuery, - answer: postAnswer, - message_id: messageId, - }) - onAdded(res.id, res.account?.name, postQuery, postAnswer) - } + try { + if (!isAdd) { + await editAnnotation(appId, annotationId, { + message_id: messageId, + question: postQuery, + answer: postAnswer, + }) + onEdited(postQuery, postAnswer) + } + else { + const res = await addAnnotation(appId, { + question: postQuery, + answer: postAnswer, + message_id: messageId, + }) + onAdded(res.id, res.account?.name ?? '', postQuery, postAnswer) + } - Toast.notify({ - message: t('common.api.actionSuccess') as string, - type: 'success', - }) + Toast.notify({ + message: t('common.api.actionSuccess') as string, + type: 'success', + }) + } + catch (error) { + const fallbackMessage = t('common.api.actionFailed') as string + const message = error instanceof Error && error.message ? error.message : fallbackMessage + Toast.notify({ + message, + type: 'error', + }) + // Re-throw to preserve edit mode behavior for UI components + throw error + } } const [showModal, setShowModal] = useState(false) diff --git a/web/app/components/app/annotation/header-opts/index.spec.tsx b/web/app/components/app/annotation/header-opts/index.spec.tsx index 8c640c2790..3d8a1fd4ef 100644 --- a/web/app/components/app/annotation/header-opts/index.spec.tsx +++ b/web/app/components/app/annotation/header-opts/index.spec.tsx @@ -1,3 +1,4 @@ +import * as React from 'react' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import type { ComponentProps } from 'react' @@ -7,6 +8,120 @@ import { LanguagesSupported } from '@/i18n-config/language' import type { AnnotationItemBasic } from '../type' import { clearAllAnnotations, fetchExportAnnotationList } from '@/service/annotation' +jest.mock('@headlessui/react', () => { + type PopoverContextValue = { open: boolean; setOpen: (open: boolean) => void } + type MenuContextValue = { open: boolean; setOpen: (open: boolean) => void } + const PopoverContext = React.createContext(null) + const MenuContext = React.createContext(null) + + const Popover = ({ children }: { children: React.ReactNode | ((props: { open: boolean }) => React.ReactNode) }) => { + const [open, setOpen] = React.useState(false) + const value = React.useMemo(() => ({ open, setOpen }), [open]) + return ( + + {typeof children === 'function' ? children({ open }) : children} + + ) + } + + const PopoverButton = React.forwardRef(({ onClick, children, ...props }: { onClick?: () => void; children?: React.ReactNode }, ref: React.Ref) => { + const context = React.useContext(PopoverContext) + const handleClick = () => { + context?.setOpen(!context.open) + onClick?.() + } + return ( + + ) + }) + + const PopoverPanel = React.forwardRef(({ children, ...props }: { children: React.ReactNode | ((props: { close: () => void }) => React.ReactNode) }, ref: React.Ref) => { + const context = React.useContext(PopoverContext) + if (!context?.open) return null + const content = typeof children === 'function' ? children({ close: () => context.setOpen(false) }) : children + return ( +
+ {content} +
+ ) + }) + + const Menu = ({ children }: { children: React.ReactNode }) => { + const [open, setOpen] = React.useState(false) + const value = React.useMemo(() => ({ open, setOpen }), [open]) + return ( + + {children} + + ) + } + + const MenuButton = ({ onClick, children, ...props }: { onClick?: () => void; children?: React.ReactNode }) => { + const context = React.useContext(MenuContext) + const handleClick = () => { + context?.setOpen(!context.open) + onClick?.() + } + return ( + + ) + } + + const MenuItems = ({ children, ...props }: { children: React.ReactNode }) => { + const context = React.useContext(MenuContext) + if (!context?.open) return null + return ( +
+ {children} +
+ ) + } + + return { + Dialog: ({ open, children, className }: { open?: boolean; children: React.ReactNode; className?: string }) => { + if (open === false) return null + return ( +
+ {children} +
+ ) + }, + DialogBackdrop: ({ children, className, onClick }: { children?: React.ReactNode; className?: string; onClick?: () => void }) => ( +
+ {children} +
+ ), + DialogPanel: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + DialogTitle: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + Popover, + PopoverButton, + PopoverPanel, + Menu, + MenuButton, + MenuItems, + Transition: ({ show = true, children }: { show?: boolean; children: React.ReactNode }) => (show ? <>{children} : null), + TransitionChild: ({ children }: { children: React.ReactNode }) => <>{children}, + } +}) + let lastCSVDownloaderProps: Record | undefined const mockCSVDownloader = jest.fn(({ children, ...props }) => { lastCSVDownloaderProps = props @@ -121,6 +236,7 @@ const mockedClearAllAnnotations = jest.mocked(clearAllAnnotations) describe('HeaderOptions', () => { beforeEach(() => { jest.clearAllMocks() + jest.useRealTimers() mockCSVDownloader.mockClear() lastCSVDownloaderProps = undefined mockedFetchAnnotations.mockResolvedValue({ data: [] }) diff --git a/web/app/components/app/annotation/type.ts b/web/app/components/app/annotation/type.ts index 5df6f51ace..e2f2264f07 100644 --- a/web/app/components/app/annotation/type.ts +++ b/web/app/components/app/annotation/type.ts @@ -12,6 +12,12 @@ export type AnnotationItem = { hit_count: number } +export type AnnotationCreateResponse = AnnotationItem & { + account?: { + name?: string + } +} + export type HitHistoryItem = { id: string question: string diff --git a/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx index b666a6cb5b..c432ca68e2 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx @@ -1,5 +1,6 @@ import * as React from 'react' -import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' +import { render, screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import ParamsConfig from './index' import ConfigContext from '@/context/debug-configuration' import type { DatasetConfigs } from '@/models/debug' @@ -11,6 +12,37 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel, } from '@/app/components/header/account-setting/model-provider-page/hooks' +jest.mock('@headlessui/react', () => ({ + Dialog: ({ children, className }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + DialogPanel: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + DialogTitle: ({ children, className, ...props }: { children: React.ReactNode; className?: string }) => ( +
+ {children} +
+ ), + Transition: ({ show, children }: { show: boolean; children: React.ReactNode }) => (show ? <>{children} : null), + TransitionChild: ({ children }: { children: React.ReactNode }) => <>{children}, + Switch: ({ checked, onChange, children, ...props }: { checked: boolean; onChange?: (value: boolean) => void; children?: React.ReactNode }) => ( + + ), +})) + jest.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useModelListAndDefaultModelAndCurrentProviderAndModel: jest.fn(), useCurrentProviderAndModel: jest.fn(), @@ -74,9 +106,6 @@ const renderParamsConfig = ({ initialModalOpen?: boolean disabled?: boolean } = {}) => { - const setDatasetConfigsSpy = jest.fn() - const setModalOpenSpy = jest.fn() - const Wrapper = ({ children }: { children: React.ReactNode }) => { const [datasetConfigsState, setDatasetConfigsState] = React.useState(datasetConfigs) const [modalOpen, setModalOpen] = React.useState(initialModalOpen) @@ -84,12 +113,10 @@ const renderParamsConfig = ({ const contextValue = { datasetConfigs: datasetConfigsState, setDatasetConfigs: (next: DatasetConfigs) => { - setDatasetConfigsSpy(next) setDatasetConfigsState(next) }, rerankSettingModalOpen: modalOpen, setRerankSettingModalOpen: (open: boolean) => { - setModalOpenSpy(open) setModalOpen(open) }, } as unknown as React.ComponentProps['value'] @@ -101,18 +128,13 @@ const renderParamsConfig = ({ ) } - render( + return render( , { wrapper: Wrapper }, ) - - return { - setDatasetConfigsSpy, - setModalOpenSpy, - } } describe('dataset-config/params-config', () => { @@ -151,77 +173,92 @@ describe('dataset-config/params-config', () => { describe('User Interactions', () => { it('should open modal and persist changes when save is clicked', async () => { // Arrange - const { setDatasetConfigsSpy } = renderParamsConfig() + renderParamsConfig() + const user = userEvent.setup() // Act - fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) + await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) const dialogScope = within(dialog) - // Change top_k via the first number input increment control. const incrementButtons = dialogScope.getAllByRole('button', { name: 'increment' }) - fireEvent.click(incrementButtons[0]) + await user.click(incrementButtons[0]) - const saveButton = await dialogScope.findByRole('button', { name: 'common.operation.save' }) - fireEvent.click(saveButton) + await waitFor(() => { + const [topKInput] = dialogScope.getAllByRole('spinbutton') + expect(topKInput).toHaveValue(5) + }) + + await user.click(dialogScope.getByRole('button', { name: 'common.operation.save' })) - // Assert - expect(setDatasetConfigsSpy).toHaveBeenCalledWith(expect.objectContaining({ top_k: 5 })) await waitFor(() => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument() }) + + await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) + const reopenedDialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) + const reopenedScope = within(reopenedDialog) + const [reopenedTopKInput] = reopenedScope.getAllByRole('spinbutton') + + // Assert + expect(reopenedTopKInput).toHaveValue(5) }) it('should discard changes when cancel is clicked', async () => { // Arrange - const { setDatasetConfigsSpy } = renderParamsConfig() + renderParamsConfig() + const user = userEvent.setup() // Act - fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) + await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) const dialogScope = within(dialog) const incrementButtons = dialogScope.getAllByRole('button', { name: 'increment' }) - fireEvent.click(incrementButtons[0]) + await user.click(incrementButtons[0]) + + await waitFor(() => { + const [topKInput] = dialogScope.getAllByRole('spinbutton') + expect(topKInput).toHaveValue(5) + }) const cancelButton = await dialogScope.findByRole('button', { name: 'common.operation.cancel' }) - fireEvent.click(cancelButton) + await user.click(cancelButton) await waitFor(() => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument() }) - // Re-open and save without changes. - fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) + // Re-open and verify the original value remains. + await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) const reopenedDialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) const reopenedScope = within(reopenedDialog) - const reopenedSave = await reopenedScope.findByRole('button', { name: 'common.operation.save' }) - fireEvent.click(reopenedSave) + const [reopenedTopKInput] = reopenedScope.getAllByRole('spinbutton') - // Assert - should save original top_k rather than the canceled change. - expect(setDatasetConfigsSpy).toHaveBeenCalledWith(expect.objectContaining({ top_k: 4 })) + // Assert + expect(reopenedTopKInput).toHaveValue(4) }) it('should prevent saving when rerank model is required but invalid', async () => { // Arrange - const { setDatasetConfigsSpy } = renderParamsConfig({ + renderParamsConfig({ datasetConfigs: createDatasetConfigs({ reranking_enable: true, reranking_mode: RerankingModeEnum.RerankingModel, }), initialModalOpen: true, }) + const user = userEvent.setup() // Act const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) const dialogScope = within(dialog) - fireEvent.click(dialogScope.getByRole('button', { name: 'common.operation.save' })) + await user.click(dialogScope.getByRole('button', { name: 'common.operation.save' })) // Assert expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'error', message: 'appDebug.datasetConfig.rerankModelRequired', }) - expect(setDatasetConfigsSpy).not.toHaveBeenCalled() expect(screen.getByRole('dialog')).toBeInTheDocument() }) }) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx index 3050249bb6..ef1fb183c8 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button.tsx @@ -41,7 +41,7 @@ const AnnotationCtrlButton: FC = ({ setShowAnnotationFullModal() return } - const res: any = await addAnnotation(appId, { + const res = await addAnnotation(appId, { message_id: messageId, question: query, answer, @@ -50,7 +50,7 @@ const AnnotationCtrlButton: FC = ({ message: t('common.api.actionSuccess') as string, type: 'success', }) - onAdded(res.id, res.account?.name) + onAdded(res.id, res.account?.name ?? '') } return ( diff --git a/web/i18n/ar-TN/common.ts b/web/i18n/ar-TN/common.ts index 8437c0643f..b1f4f46f22 100644 --- a/web/i18n/ar-TN/common.ts +++ b/web/i18n/ar-TN/common.ts @@ -11,6 +11,7 @@ const translation = { saved: 'تم الحفظ', create: 'تم الإنشاء', remove: 'تمت الإزالة', + actionFailed: 'فشل الإجراء', }, operation: { create: 'إنشاء', diff --git a/web/i18n/de-DE/common.ts b/web/i18n/de-DE/common.ts index 479348ef43..d9ebfd60e0 100644 --- a/web/i18n/de-DE/common.ts +++ b/web/i18n/de-DE/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Gespeichert', create: 'Erstellt', remove: 'Entfernt', + actionFailed: 'Aktion fehlgeschlagen', }, operation: { create: 'Erstellen', diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index d78520cf1f..92d24b1351 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -8,6 +8,7 @@ const translation = { api: { success: 'Success', actionSuccess: 'Action succeeded', + actionFailed: 'Action failed', saved: 'Saved', create: 'Created', remove: 'Removed', diff --git a/web/i18n/es-ES/common.ts b/web/i18n/es-ES/common.ts index 38d4402fd2..8f56c7e668 100644 --- a/web/i18n/es-ES/common.ts +++ b/web/i18n/es-ES/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Guardado', create: 'Creado', remove: 'Eliminado', + actionFailed: 'Acción fallida', }, operation: { create: 'Crear', diff --git a/web/i18n/fa-IR/common.ts b/web/i18n/fa-IR/common.ts index 62a2e2cec8..afd6f760aa 100644 --- a/web/i18n/fa-IR/common.ts +++ b/web/i18n/fa-IR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'ذخیره شد', create: 'ایجاد شد', remove: 'حذف شد', + actionFailed: 'عمل شکست خورد', }, operation: { create: 'ایجاد', diff --git a/web/i18n/fr-FR/common.ts b/web/i18n/fr-FR/common.ts index da72b0497c..4b0deb4a8c 100644 --- a/web/i18n/fr-FR/common.ts +++ b/web/i18n/fr-FR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Sauvegardé', create: 'Créé', remove: 'Supprimé', + actionFailed: 'Action échouée', }, operation: { create: 'Créer', diff --git a/web/i18n/hi-IN/common.ts b/web/i18n/hi-IN/common.ts index fa25074b9c..88f8f814e6 100644 --- a/web/i18n/hi-IN/common.ts +++ b/web/i18n/hi-IN/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'सहेजा गया', create: 'बनाया गया', remove: 'हटाया गया', + actionFailed: 'क्रिया विफल', }, operation: { create: 'बनाएं', diff --git a/web/i18n/id-ID/common.ts b/web/i18n/id-ID/common.ts index 0c70b0341e..4cce24e76a 100644 --- a/web/i18n/id-ID/common.ts +++ b/web/i18n/id-ID/common.ts @@ -11,6 +11,7 @@ const translation = { remove: 'Dihapus', actionSuccess: 'Aksi berhasil', create: 'Dibuat', + actionFailed: 'Tindakan gagal', }, operation: { setup: 'Setup', diff --git a/web/i18n/it-IT/common.ts b/web/i18n/it-IT/common.ts index d5793bb902..b52b93b1a5 100644 --- a/web/i18n/it-IT/common.ts +++ b/web/i18n/it-IT/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Salvato', create: 'Creato', remove: 'Rimosso', + actionFailed: 'Azione non riuscita', }, operation: { create: 'Crea', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index d4647fbc12..bde00cb66b 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -11,6 +11,7 @@ const translation = { saved: '保存済み', create: '作成済み', remove: '削除済み', + actionFailed: 'アクションに失敗しました', }, operation: { create: '作成', diff --git a/web/i18n/ko-KR/common.ts b/web/i18n/ko-KR/common.ts index 805b9f9840..531aa29054 100644 --- a/web/i18n/ko-KR/common.ts +++ b/web/i18n/ko-KR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: '저장됨', create: '생성됨', remove: '삭제됨', + actionFailed: '작업 실패', }, operation: { create: '생성', diff --git a/web/i18n/pl-PL/common.ts b/web/i18n/pl-PL/common.ts index 2ecf18c7e6..10f566258a 100644 --- a/web/i18n/pl-PL/common.ts +++ b/web/i18n/pl-PL/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Zapisane', create: 'Utworzono', remove: 'Usunięto', + actionFailed: 'Akcja nie powiodła się', }, operation: { create: 'Utwórz', diff --git a/web/i18n/pt-BR/common.ts b/web/i18n/pt-BR/common.ts index d0838b4f09..b739561ca4 100644 --- a/web/i18n/pt-BR/common.ts +++ b/web/i18n/pt-BR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Salvo', create: 'Criado', remove: 'Removido', + actionFailed: 'Ação falhou', }, operation: { create: 'Criar', diff --git a/web/i18n/ro-RO/common.ts b/web/i18n/ro-RO/common.ts index 8b8ab9ac26..df3cf01b6c 100644 --- a/web/i18n/ro-RO/common.ts +++ b/web/i18n/ro-RO/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Salvat', create: 'Creat', remove: 'Eliminat', + actionFailed: 'Acțiunea a eșuat', }, operation: { create: 'Creează', diff --git a/web/i18n/ru-RU/common.ts b/web/i18n/ru-RU/common.ts index afc9368e9e..ae8b2e558f 100644 --- a/web/i18n/ru-RU/common.ts +++ b/web/i18n/ru-RU/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Сохранено', create: 'Создано', remove: 'Удалено', + actionFailed: 'Действие не удалось', }, operation: { create: 'Создать', diff --git a/web/i18n/sl-SI/common.ts b/web/i18n/sl-SI/common.ts index b024ace3be..697d06eb8b 100644 --- a/web/i18n/sl-SI/common.ts +++ b/web/i18n/sl-SI/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Shranjeno', create: 'Ustvarjeno', remove: 'Odstranjeno', + actionFailed: 'Dejanje ni uspelo', }, operation: { create: 'Ustvari', diff --git a/web/i18n/th-TH/common.ts b/web/i18n/th-TH/common.ts index 9c325e3781..dc82c71c78 100644 --- a/web/i18n/th-TH/common.ts +++ b/web/i18n/th-TH/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'บันทึก', create: 'สร้าง', remove: 'ถูก เอา ออก', + actionFailed: 'การดำเนินการล้มเหลว', }, operation: { create: 'สร้าง', diff --git a/web/i18n/tr-TR/common.ts b/web/i18n/tr-TR/common.ts index 8b0a7cba69..5e7f2182c7 100644 --- a/web/i18n/tr-TR/common.ts +++ b/web/i18n/tr-TR/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Kaydedildi', create: 'Oluşturuldu', remove: 'Kaldırıldı', + actionFailed: 'İşlem başarısız', }, operation: { create: 'Oluştur', diff --git a/web/i18n/uk-UA/common.ts b/web/i18n/uk-UA/common.ts index bd0f55c2f5..70b8aaa862 100644 --- a/web/i18n/uk-UA/common.ts +++ b/web/i18n/uk-UA/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Збережено', create: 'Створено', remove: 'Видалено', + actionFailed: 'Не вдалося виконати дію', }, operation: { create: 'Створити', diff --git a/web/i18n/vi-VN/common.ts b/web/i18n/vi-VN/common.ts index 8b1b69163e..666dc7a133 100644 --- a/web/i18n/vi-VN/common.ts +++ b/web/i18n/vi-VN/common.ts @@ -5,6 +5,7 @@ const translation = { saved: 'Đã lưu', create: 'Tạo', remove: 'Xóa', + actionFailed: 'Thao tác thất bại', }, operation: { create: 'Tạo mới', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 8e7103564f..bd0e0e3ba4 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -11,6 +11,7 @@ const translation = { saved: '已保存', create: '已创建', remove: '已移除', + actionFailed: '操作失败', }, operation: { create: '创建', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 1a72a083d8..8ed1e336ef 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -5,6 +5,7 @@ const translation = { saved: '已儲存', create: '已建立', remove: '已移除', + actionFailed: '操作失敗', }, operation: { create: '建立', diff --git a/web/jest.config.ts b/web/jest.config.ts index e86ec5af74..434b19270f 100644 --- a/web/jest.config.ts +++ b/web/jest.config.ts @@ -20,7 +20,7 @@ const config: Config = { // bail: 0, // The directory where Jest should store its cached dependency information - // cacheDirectory: "/private/var/folders/9c/7gly5yl90qxdjljqsvkk758h0000gn/T/jest_dx", + cacheDirectory: '/.cache/jest', // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, diff --git a/web/service/annotation.ts b/web/service/annotation.ts index 58efb7b976..af708fe174 100644 --- a/web/service/annotation.ts +++ b/web/service/annotation.ts @@ -1,6 +1,6 @@ import type { Fetcher } from 'swr' import { del, get, post } from './base' -import type { AnnotationEnableStatus, AnnotationItemBasic, EmbeddingModelConfig } from '@/app/components/app/annotation/type' +import type { AnnotationCreateResponse, AnnotationEnableStatus, AnnotationItemBasic, EmbeddingModelConfig } from '@/app/components/app/annotation/type' import { ANNOTATION_DEFAULT } from '@/config' export const fetchAnnotationConfig = (appId: string) => { @@ -41,7 +41,7 @@ export const fetchExportAnnotationList = (appId: string) => { } export const addAnnotation = (appId: string, body: AnnotationItemBasic) => { - return post(`apps/${appId}/annotations`, { body }) + return post(`apps/${appId}/annotations`, { body }) } export const annotationBatchImport: Fetcher<{ job_id: string; job_status: string }, { url: string; body: FormData }> = ({ url, body }) => { From a26881cb246ca503e8a58654b0cb9d27738351e0 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 19 Dec 2025 12:08:34 +0800 Subject: [PATCH 08/17] refactor: unified cn utils (#29916) Co-authored-by: yyh Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> --- .../(appDetailLayout)/[appId]/layout-main.tsx | 2 +- .../time-range-picker/date-picker.tsx | 2 +- .../time-range-picker/range-selector.tsx | 2 +- .../overview/tracing/config-button.tsx | 2 +- .../[appId]/overview/tracing/config-popup.tsx | 2 +- .../[appId]/overview/tracing/field.tsx | 2 +- .../[appId]/overview/tracing/panel.tsx | 2 +- .../overview/tracing/provider-panel.tsx | 2 +- .../[appId]/overview/tracing/tracing-icon.tsx | 2 +- .../[datasetId]/layout-main.tsx | 2 +- .../webapp-reset-password/layout.tsx | 2 +- .../set-password/page.tsx | 2 +- .../(shareLayout)/webapp-signin/layout.tsx | 2 +- .../webapp-signin/normalForm.tsx | 2 +- web/app/account/oauth/authorize/layout.tsx | 2 +- web/app/activate/activateForm.tsx | 2 +- web/app/activate/page.tsx | 2 +- web/app/components/app-sidebar/app-info.tsx | 2 +- .../app-sidebar/app-sidebar-dropdown.tsx | 2 +- .../app-sidebar/dataset-info/dropdown.tsx | 2 +- .../app-sidebar/dataset-info/index.tsx | 2 +- .../app-sidebar/dataset-sidebar-dropdown.tsx | 2 +- web/app/components/app-sidebar/index.tsx | 2 +- web/app/components/app-sidebar/navLink.tsx | 32 +++++-------- .../components/app-sidebar/toggle-button.tsx | 2 +- .../app/annotation/batch-action.tsx | 4 +- .../csv-uploader.tsx | 2 +- .../edit-annotation-modal/edit-item/index.tsx | 2 +- .../app/annotation/header-opts/index.tsx | 2 +- web/app/components/app/annotation/index.tsx | 2 +- web/app/components/app/annotation/list.tsx | 2 +- .../view-annotation-modal/index.tsx | 2 +- .../access-control-dialog.tsx | 2 +- .../add-member-or-group-pop.tsx | 6 +-- .../app/app-publisher/suggested-action.tsx | 8 ++-- .../base/feature-panel/index.tsx | 2 +- .../base/operation-btn/index.tsx | 2 +- .../config-prompt/advanced-prompt-input.tsx | 2 +- .../config-prompt/message-type-selector.tsx | 2 +- .../prompt-editor-height-resize-wrap.tsx | 2 +- .../config-prompt/simple-prompt-input.tsx | 2 +- .../config-var/config-modal/field.tsx | 2 +- .../config-var/config-modal/type-select.tsx | 7 ++- .../config-var/config-select/index.tsx | 2 +- .../app/configuration/config-var/index.tsx | 2 +- .../config-var/select-type-item/index.tsx | 2 +- .../app/configuration/config-var/var-item.tsx | 2 +- .../config-vision/param-config.tsx | 2 +- .../config/agent/agent-setting/item-panel.tsx | 2 +- .../config/agent/agent-tools/index.tsx | 2 +- .../agent-tools/setting-built-in-tool.tsx | 2 +- .../config/agent/prompt-editor.tsx | 2 +- .../config/assistant-type-picker/index.tsx | 2 +- .../config/automatic/idea-output.tsx | 2 +- .../config/automatic/instruction-editor.tsx | 2 +- .../config/automatic/prompt-toast.tsx | 2 +- .../config/automatic/version-selector.tsx | 2 +- .../dataset-config/card-item/index.tsx | 2 +- .../dataset-config/context-var/index.tsx | 2 +- .../dataset-config/context-var/var-picker.tsx | 2 +- .../params-config/config-content.tsx | 2 +- .../dataset-config/params-config/index.tsx | 2 +- .../params-config/weighted-score.tsx | 2 +- .../dataset-config/select-dataset/index.tsx | 2 +- .../dataset-config/settings-modal/index.tsx | 2 +- .../settings-modal/retrieval-section.tsx | 2 +- .../configuration/debug/chat-user-input.tsx | 2 +- .../prompt-value-panel/index.tsx | 2 +- .../app/create-app-dialog/app-card/index.tsx | 2 +- .../app/create-app-dialog/app-list/index.tsx | 2 +- .../create-app-dialog/app-list/sidebar.tsx | 6 +-- .../components/app/create-app-modal/index.tsx | 2 +- .../app/create-from-dsl-modal/index.tsx | 2 +- .../app/create-from-dsl-modal/uploader.tsx | 2 +- .../components/app/duplicate-modal/index.tsx | 2 +- .../components/app/log-annotation/index.tsx | 2 +- web/app/components/app/log/list.tsx | 2 +- web/app/components/app/log/model-info.tsx | 2 +- web/app/components/app/log/var-panel.tsx | 2 +- .../app/overview/apikey-info-panel/index.tsx | 2 +- .../app/overview/embedded/index.tsx | 2 +- .../app/overview/settings/index.tsx | 2 +- .../components/app/switch-app-modal/index.tsx | 2 +- .../app/text-generate/item/index.tsx | 2 +- .../app/text-generate/saved-items/index.tsx | 2 +- .../components/app/type-selector/index.tsx | 2 +- web/app/components/app/workflow-log/list.tsx | 2 +- web/app/components/apps/app-card.tsx | 2 +- web/app/components/apps/new-app-card.tsx | 2 +- .../components/base/action-button/index.tsx | 8 ++-- .../base/agent-log-modal/detail.tsx | 2 +- .../components/base/agent-log-modal/index.tsx | 2 +- .../base/agent-log-modal/iteration.tsx | 2 +- .../base/agent-log-modal/tool-call.tsx | 2 +- web/app/components/base/answer-icon/index.tsx | 8 ++-- .../base/app-icon-picker/ImageInput.tsx | 7 ++- .../components/base/app-icon-picker/index.tsx | 2 +- web/app/components/base/app-icon/index.tsx | 4 +- web/app/components/base/app-unavailable.tsx | 4 +- .../base/audio-gallery/AudioPlayer.tsx | 2 +- .../base/auto-height-textarea/index.tsx | 2 +- web/app/components/base/avatar/index.tsx | 2 +- web/app/components/base/badge.tsx | 2 +- web/app/components/base/badge/index.tsx | 8 ++-- web/app/components/base/block-input/index.tsx | 12 ++--- web/app/components/base/button/add-button.tsx | 2 +- web/app/components/base/button/index.tsx | 10 ++-- .../components/base/button/sync-button.tsx | 2 +- .../chat/chat-with-history/chat-wrapper.tsx | 2 +- .../chat/chat-with-history/header/index.tsx | 2 +- .../chat-with-history/header/operation.tsx | 2 +- .../base/chat/chat-with-history/index.tsx | 2 +- .../chat-with-history/inputs-form/index.tsx | 2 +- .../chat/chat-with-history/sidebar/index.tsx | 2 +- .../chat/chat-with-history/sidebar/item.tsx | 2 +- .../chat-with-history/sidebar/operation.tsx | 2 +- .../base/chat/chat/answer/basic-content.tsx | 2 +- .../base/chat/chat/answer/index.tsx | 2 +- .../base/chat/chat/answer/operation.tsx | 2 +- .../base/chat/chat/answer/tool-detail.tsx | 2 +- .../chat/chat/answer/workflow-process.tsx | 2 +- .../base/chat/chat/chat-input-area/index.tsx | 2 +- .../chat/chat/chat-input-area/operation.tsx | 2 +- web/app/components/base/chat/chat/index.tsx | 2 +- .../base/chat/chat/loading-anim/index.tsx | 2 +- .../components/base/chat/chat/question.tsx | 2 +- .../chat/embedded-chatbot/chat-wrapper.tsx | 2 +- .../chat/embedded-chatbot/header/index.tsx | 2 +- .../base/chat/embedded-chatbot/index.tsx | 2 +- .../embedded-chatbot/inputs-form/index.tsx | 2 +- .../inputs-form/view-form-dropdown.tsx | 2 +- .../components/base/checkbox-list/index.tsx | 2 +- web/app/components/base/checkbox/index.tsx | 2 +- web/app/components/base/chip/index.tsx | 2 +- .../components/base/content-dialog/index.tsx | 14 ++---- .../components/base/corner-label/index.tsx | 2 +- .../date-and-time-picker/calendar/item.tsx | 2 +- .../common/option-list-item.tsx | 2 +- .../date-picker/footer.tsx | 2 +- .../date-picker/index.tsx | 2 +- .../time-picker/index.tsx | 2 +- web/app/components/base/dialog/index.tsx | 20 ++++---- web/app/components/base/divider/index.tsx | 4 +- web/app/components/base/drawer-plus/index.tsx | 2 +- web/app/components/base/drawer/index.tsx | 2 +- web/app/components/base/dropdown/index.tsx | 2 +- web/app/components/base/effect/index.tsx | 2 +- .../components/base/emoji-picker/Inner.tsx | 2 +- .../components/base/emoji-picker/index.tsx | 2 +- .../base/encrypted-bottom/index.tsx | 2 +- .../components/base/error-boundary/index.tsx | 2 +- .../score-slider/base-slider/index.tsx | 2 +- .../conversation-opener/modal.tsx | 2 +- .../new-feature-panel/dialog-wrapper.tsx | 2 +- .../new-feature-panel/feature-bar.tsx | 2 +- .../moderation/moderation-setting-modal.tsx | 2 +- .../text-to-speech/param-config-content.tsx | 18 +++----- web/app/components/base/file-thumb/index.tsx | 2 +- .../file-from-link-or-local/index.tsx | 2 +- .../base/file-uploader/file-image-render.tsx | 2 +- .../base/file-uploader/file-list-in-log.tsx | 2 +- .../base/file-uploader/file-type-icon.tsx | 2 +- .../file-uploader-in-attachment/file-item.tsx | 2 +- .../file-uploader-in-attachment/index.tsx | 2 +- .../file-uploader-in-chat-input/file-item.tsx | 2 +- .../file-uploader-in-chat-input/file-list.tsx | 2 +- .../file-uploader-in-chat-input/index.tsx | 2 +- .../base/form/components/base/base-field.tsx | 2 +- .../base/form/components/base/base-form.tsx | 2 +- .../base/form/components/field/checkbox.tsx | 2 +- .../form/components/field/custom-select.tsx | 2 +- .../base/form/components/field/file-types.tsx | 2 +- .../form/components/field/file-uploader.tsx | 2 +- .../field/input-type-select/index.tsx | 2 +- .../field/input-type-select/trigger.tsx | 2 +- .../field/mixed-variable-text-input/index.tsx | 2 +- .../form/components/field/number-input.tsx | 2 +- .../form/components/field/number-slider.tsx | 2 +- .../base/form/components/field/options.tsx | 2 +- .../base/form/components/field/select.tsx | 2 +- .../base/form/components/field/text-area.tsx | 2 +- .../base/form/components/field/text.tsx | 2 +- .../form/components/field/upload-method.tsx | 2 +- .../field/variable-or-constant-input.tsx | 2 +- .../components/field/variable-selector.tsx | 2 +- .../components/base/form/components/label.tsx | 2 +- .../base/fullscreen-modal/index.tsx | 16 +++---- web/app/components/base/grid-mask/index.tsx | 8 ++-- web/app/components/base/icons/script.mjs | 2 +- .../icons/src/image/llm/BaichuanTextCn.tsx | 2 +- .../base/icons/src/image/llm/Minimax.tsx | 2 +- .../base/icons/src/image/llm/MinimaxText.tsx | 2 +- .../base/icons/src/image/llm/Tongyi.tsx | 2 +- .../base/icons/src/image/llm/TongyiText.tsx | 2 +- .../base/icons/src/image/llm/TongyiTextCn.tsx | 2 +- .../base/icons/src/image/llm/Wxyy.tsx | 2 +- .../base/icons/src/image/llm/WxyyText.tsx | 2 +- .../base/icons/src/image/llm/WxyyTextCn.tsx | 2 +- .../components/base/image-gallery/index.tsx | 2 +- .../image-uploader/chat-image-uploader.tsx | 2 +- .../base/image-uploader/image-list.tsx | 2 +- .../base/inline-delete-confirm/index.tsx | 2 +- .../components/base/input-number/index.tsx | 21 ++++----- .../components/base/input-with-copy/index.tsx | 2 +- web/app/components/base/input/index.tsx | 2 +- .../base/linked-apps-panel/index.tsx | 2 +- web/app/components/base/logo/dify-logo.tsx | 4 +- .../base/logo/logo-embedded-chat-header.tsx | 4 +- web/app/components/base/logo/logo-site.tsx | 4 +- .../base/markdown-blocks/button.tsx | 2 +- .../base/markdown-blocks/think-block.tsx | 2 +- web/app/components/base/markdown/index.tsx | 2 +- web/app/components/base/mermaid/index.tsx | 2 +- .../base/message-log-modal/index.tsx | 2 +- .../components/base/modal-like-wrap/index.tsx | 2 +- web/app/components/base/modal/index.tsx | 18 +++----- web/app/components/base/modal/modal.tsx | 2 +- web/app/components/base/node-status/index.tsx | 12 ++--- web/app/components/base/notion-icon/index.tsx | 2 +- .../page-selector/index.tsx | 2 +- .../search-input/index.tsx | 2 +- web/app/components/base/pagination/index.tsx | 2 +- .../components/base/pagination/pagination.tsx | 2 +- web/app/components/base/popover/index.tsx | 2 +- .../base/portal-to-follow-elem/index.tsx | 2 +- .../components/base/premium-badge/index.tsx | 14 ++---- .../base/progress-bar/progress-circle.tsx | 2 +- .../components/base/prompt-editor/index.tsx | 2 +- .../plugins/current-block/component.tsx | 2 +- .../plugins/error-message-block/component.tsx | 2 +- .../plugins/last-run-block/component.tsx | 2 +- .../prompt-editor/plugins/placeholder.tsx | 2 +- web/app/components/base/radio-card/index.tsx | 2 +- .../base/radio-card/simple/index.tsx | 2 +- .../base/radio/component/group/index.tsx | 2 +- .../base/radio/component/radio/index.tsx | 2 +- web/app/components/base/radio/ui.tsx | 2 +- .../components/base/search-input/index.tsx | 2 +- .../base/segmented-control/index.tsx | 2 +- web/app/components/base/select/custom.tsx | 2 +- web/app/components/base/select/index.tsx | 46 ++++++++----------- web/app/components/base/select/pure.tsx | 2 +- .../base/simple-pie-chart/index.tsx | 4 +- web/app/components/base/skeleton/index.tsx | 10 ++-- web/app/components/base/slider/index.tsx | 2 +- web/app/components/base/sort/index.tsx | 2 +- web/app/components/base/svg/index.tsx | 2 +- web/app/components/base/switch/index.tsx | 14 ++---- web/app/components/base/tab-header/index.tsx | 2 +- .../components/base/tab-slider-new/index.tsx | 2 +- .../base/tab-slider-plain/index.tsx | 2 +- web/app/components/base/tab-slider/index.tsx | 2 +- web/app/components/base/tag-input/index.tsx | 2 +- .../components/base/tag-management/filter.tsx | 2 +- .../base/tag-management/selector.tsx | 2 +- .../base/tag-management/tag-item-editor.tsx | 2 +- .../base/tag-management/tag-remove-modal.tsx | 2 +- web/app/components/base/tag/index.tsx | 4 +- web/app/components/base/textarea/index.tsx | 2 +- web/app/components/base/theme-switcher.tsx | 2 +- .../components/base/timezone-label/index.tsx | 2 +- web/app/components/base/toast/index.tsx | 2 +- web/app/components/base/tooltip/index.tsx | 2 +- web/app/components/base/voice-input/index.tsx | 2 +- .../billing/annotation-full/index.tsx | 2 +- .../billing/annotation-full/modal.tsx | 2 +- .../billing/apps-full-in-dialog/index.tsx | 2 +- .../billing/header-billing-btn/index.tsx | 2 +- web/app/components/billing/pricing/footer.tsx | 2 +- .../billing/pricing/plan-switcher/tab.tsx | 2 +- .../pricing/plans/cloud-plan-item/button.tsx | 2 +- .../plans/self-hosted-plan-item/button.tsx | 2 +- .../plans/self-hosted-plan-item/index.tsx | 2 +- .../billing/priority-label/index.tsx | 2 +- .../components/billing/progress-bar/index.tsx | 2 +- .../components/billing/usage-info/index.tsx | 2 +- .../billing/vector-space-full/index.tsx | 2 +- .../custom/custom-web-app-brand/index.tsx | 2 +- .../datasets/common/credential-icon.tsx | 2 +- .../common/document-picker/document-list.tsx | 2 +- .../datasets/common/document-picker/index.tsx | 2 +- .../preview-document-picker.tsx | 2 +- .../status-with-action.tsx | 2 +- .../datasets/common/image-list/index.tsx | 2 +- .../image-uploader-in-chunk/image-input.tsx | 2 +- .../image-uploader-in-chunk/index.tsx | 2 +- .../index.tsx | 2 +- .../common/retrieval-param-config/index.tsx | 2 +- .../create-from-dsl-modal/tab/item.tsx | 2 +- .../create-from-dsl-modal/uploader.tsx | 2 +- .../details/chunk-structure-card.tsx | 2 +- .../create/embedding-process/index.tsx | 2 +- .../empty-dataset-creation-modal/index.tsx | 2 +- .../datasets/create/file-preview/index.tsx | 2 +- .../datasets/create/file-uploader/index.tsx | 2 +- .../create/notion-page-preview/index.tsx | 2 +- .../datasets/create/step-one/index.tsx | 7 ++- .../datasets/create/step-two/index.tsx | 2 +- .../create/step-two/language-select/index.tsx | 2 +- .../datasets/create/step-two/option-card.tsx | 16 +++---- .../datasets/create/stepper/step.tsx | 19 +++----- .../create/stop-embedding-modal/index.tsx | 2 +- .../datasets/create/top-bar/index.tsx | 4 +- .../website/base/checkbox-with-label.tsx | 2 +- .../website/base/crawled-result-item.tsx | 2 +- .../create/website/base/crawled-result.tsx | 2 +- .../create/website/base/error-message.tsx | 2 +- .../datasets/create/website/base/field.tsx | 2 +- .../datasets/create/website/base/header.tsx | 2 +- .../create/website/base/options-wrap.tsx | 2 +- .../create/website/firecrawl/options.tsx | 2 +- .../datasets/create/website/index.tsx | 2 +- .../create/website/jina-reader/options.tsx | 2 +- .../datasets/create/website/preview.tsx | 2 +- .../create/website/watercrawl/options.tsx | 2 +- .../data-source-options/datasource-icon.tsx | 2 +- .../data-source-options/option-card.tsx | 2 +- .../base/credential-selector/trigger.tsx | 2 +- .../data-source/local-file/index.tsx | 2 +- .../online-documents/page-selector/item.tsx | 2 +- .../file-list/header/breadcrumbs/bucket.tsx | 2 +- .../file-list/header/breadcrumbs/drive.tsx | 2 +- .../header/breadcrumbs/dropdown/index.tsx | 2 +- .../file-list/header/breadcrumbs/item.tsx | 2 +- .../online-drive/file-list/list/file-icon.tsx | 2 +- .../online-drive/file-list/list/item.tsx | 2 +- .../base/checkbox-with-label.tsx | 2 +- .../base/crawled-result-item.tsx | 2 +- .../website-crawl/base/crawled-result.tsx | 2 +- .../website-crawl/base/crawling.tsx | 2 +- .../website-crawl/base/error-message.tsx | 2 +- .../website-crawl/base/options/index.tsx | 2 +- .../processing/embedding-process/index.tsx | 2 +- .../create-from-pipeline/step-indicator.tsx | 2 +- .../detail/batch-modal/csv-uploader.tsx | 2 +- .../detail/completed/child-segment-detail.tsx | 8 ++-- .../detail/completed/child-segment-list.tsx | 2 +- .../detail/completed/common/add-another.tsx | 4 +- .../detail/completed/common/batch-action.tsx | 2 +- .../detail/completed/common/chunk-content.tsx | 14 ++---- .../detail/completed/common/drawer.tsx | 2 +- .../completed/common/full-screen-drawer.tsx | 2 +- .../detail/completed/common/keywords.tsx | 4 +- .../completed/common/segment-index-tag.tsx | 2 +- .../documents/detail/completed/common/tag.tsx | 2 +- .../documents/detail/completed/index.tsx | 2 +- .../detail/completed/new-child-segment.tsx | 8 ++-- .../completed/segment-card/chunk-content.tsx | 2 +- .../detail/completed/segment-card/index.tsx | 2 +- .../detail/completed/segment-detail.tsx | 2 +- .../documents/detail/document-title.tsx | 2 +- .../documents/detail/embedding/index.tsx | 2 +- .../datasets/documents/detail/index.tsx | 2 +- .../documents/detail/metadata/index.tsx | 2 +- .../datasets/documents/detail/new-segment.tsx | 14 +++--- .../documents/detail/segment-add/index.tsx | 2 +- .../components/datasets/documents/index.tsx | 2 +- .../components/datasets/documents/list.tsx | 2 +- .../datasets/documents/operations.tsx | 2 +- .../datasets/documents/status-item/index.tsx | 2 +- .../external-api/external-api-modal/Form.tsx | 2 +- .../external-api/external-api-panel/index.tsx | 2 +- .../create/RetrievalSettings.tsx | 2 +- .../datasets/extra-info/service-api/card.tsx | 2 +- .../datasets/extra-info/service-api/index.tsx | 2 +- .../formatted-text/flavours/edit-slice.tsx | 20 +++----- .../formatted-text/flavours/shared.tsx | 28 ++++------- .../datasets/formatted-text/formatted.tsx | 4 +- .../components/chunk-detail-modal.tsx | 2 +- .../datasets/hit-testing/components/mask.tsx | 2 +- .../components/query-input/index.tsx | 2 +- .../components/query-input/textarea.tsx | 2 +- .../hit-testing/components/records.tsx | 2 +- .../components/result-item-external.tsx | 2 +- .../components/result-item-meta.tsx | 2 +- .../hit-testing/components/result-item.tsx | 2 +- .../datasets/hit-testing/components/score.tsx | 2 +- .../components/datasets/hit-testing/index.tsx | 2 +- .../datasets/list/dataset-card/index.tsx | 2 +- .../datasets/metadata/add-metadata-button.tsx | 2 +- .../datasets/metadata/base/date-picker.tsx | 2 +- .../metadata/edit-metadata-batch/add-row.tsx | 2 +- .../metadata/edit-metadata-batch/edit-row.tsx | 2 +- .../edit-metadata-batch/input-combined.tsx | 2 +- .../input-has-set-multiple-value.tsx | 2 +- .../metadata/edit-metadata-batch/label.tsx | 2 +- .../dataset-metadata-drawer.tsx | 2 +- .../metadata/metadata-document/index.tsx | 2 +- .../metadata/metadata-document/info-group.tsx | 2 +- .../components/datasets/preview/container.tsx | 8 ++-- .../components/datasets/preview/header.tsx | 6 +-- .../datasets/rename-modal/index.tsx | 2 +- .../datasets/settings/index-method/index.tsx | 4 +- .../datasets/settings/option-card.tsx | 2 +- .../settings/permission-selector/index.tsx | 2 +- .../permission-selector/member-item.tsx | 2 +- web/app/components/develop/code.tsx | 26 ++++------- web/app/components/develop/doc.tsx | 2 +- web/app/components/develop/md.tsx | 8 ++-- web/app/components/develop/tag.tsx | 8 ++-- web/app/components/explore/app-card/index.tsx | 2 +- web/app/components/explore/app-list/index.tsx | 2 +- web/app/components/explore/category.tsx | 2 +- .../explore/item-operation/index.tsx | 2 +- .../explore/sidebar/app-nav-item/index.tsx | 2 +- web/app/components/explore/sidebar/index.tsx | 2 +- .../goto-anything/actions/knowledge.tsx | 2 +- .../header/account-dropdown/compliance.tsx | 2 +- .../header/account-dropdown/index.tsx | 2 +- .../header/account-dropdown/support.tsx | 2 +- .../workplace-selector/index.tsx | 2 +- .../Integrations-page/index.tsx | 4 +- .../header/account-setting/collapse/index.tsx | 4 +- .../install-from-marketplace.tsx | 2 +- .../data-source-notion/operate/index.tsx | 2 +- .../data-source-website/index.tsx | 2 +- .../data-source-page/panel/config-item.tsx | 2 +- .../data-source-page/panel/index.tsx | 2 +- .../header/account-setting/index.tsx | 2 +- .../edit-workspace-modal/index.tsx | 2 +- .../account-setting/members-page/index.tsx | 2 +- .../members-page/invite-modal/index.tsx | 2 +- .../invite-modal/role-selector.tsx | 2 +- .../members-page/operation/index.tsx | 2 +- .../operation/transfer-ownership.tsx | 2 +- .../member-selector.tsx | 2 +- .../header/account-setting/menu-dialog.tsx | 2 +- .../model-provider-page/index.tsx | 2 +- .../install-from-marketplace.tsx | 2 +- .../add-credential-in-load-balancing.tsx | 2 +- .../model-auth/add-custom-model.tsx | 2 +- .../model-auth/authorized/credential-item.tsx | 2 +- .../model-auth/authorized/index.tsx | 2 +- .../model-auth/config-model.tsx | 2 +- .../manage-custom-model-credentials.tsx | 2 +- .../switch-credential-in-load-balancing.tsx | 2 +- .../model-provider-page/model-badge/index.tsx | 8 ++-- .../model-provider-page/model-icon/index.tsx | 2 +- .../model-provider-page/model-modal/Form.tsx | 2 +- .../model-provider-page/model-name/index.tsx | 2 +- .../agent-model-trigger.tsx | 2 +- .../model-parameter-modal/index.tsx | 2 +- .../model-parameter-modal/parameter-item.tsx | 2 +- .../presets-parameter.tsx | 2 +- .../model-parameter-modal/trigger.tsx | 2 +- .../deprecated-model-trigger.tsx | 2 +- .../model-selector/empty-trigger.tsx | 2 +- .../model-selector/feature-icon.tsx | 2 +- .../model-selector/index.tsx | 4 +- .../model-selector/model-trigger.tsx | 2 +- .../model-selector/popup-item.tsx | 2 +- .../provider-added-card/add-model-button.tsx | 2 +- .../provider-added-card/credential-panel.tsx | 2 +- .../provider-added-card/index.tsx | 2 +- .../provider-added-card/model-list-item.tsx | 8 ++-- .../model-load-balancing-configs.tsx | 10 ++-- .../model-load-balancing-modal.tsx | 8 ++-- .../provider-added-card/priority-selector.tsx | 2 +- .../provider-icon/index.tsx | 2 +- web/app/components/header/app-back/index.tsx | 4 +- .../components/header/explore-nav/index.tsx | 8 ++-- web/app/components/header/header-wrapper.tsx | 8 ++-- web/app/components/header/indicator/index.tsx | 8 ++-- web/app/components/header/nav/index.tsx | 8 ++-- .../header/nav/nav-selector/index.tsx | 2 +- .../components/header/plugins-nav/index.tsx | 12 ++--- web/app/components/header/tools-nav/index.tsx | 8 ++-- .../plugins/base/badges/icon-with-tooltip.tsx | 2 +- .../plugins/base/deprecation-notice.tsx | 2 +- .../plugins/base/key-value-item.tsx | 2 +- .../plugins/card/base/card-icon.tsx | 2 +- .../plugins/card/base/description.tsx | 2 +- .../components/plugins/card/base/org-info.tsx | 2 +- .../plugins/card/base/placeholder.tsx | 2 +- web/app/components/plugins/card/index.tsx | 2 +- .../install-plugin/install-bundle/index.tsx | 2 +- .../install-from-github/index.tsx | 2 +- .../install-from-local-package/index.tsx | 2 +- .../install-from-marketplace/index.tsx | 2 +- .../plugins/marketplace/empty/index.tsx | 2 +- .../plugins/marketplace/list/index.tsx | 2 +- .../marketplace/list/list-with-collection.tsx | 2 +- .../marketplace/plugin-type-switch.tsx | 2 +- .../plugins/marketplace/search-box/index.tsx | 2 +- .../search-box/trigger/marketplace.tsx | 2 +- .../search-box/trigger/tool-selector.tsx | 2 +- .../sticky-search-and-switch-wrapper.tsx | 2 +- .../authorize/add-oauth-button.tsx | 2 +- .../plugins/plugin-auth/authorize/index.tsx | 2 +- .../authorized-in-data-source-node.tsx | 2 +- .../plugin-auth/authorized-in-node.tsx | 2 +- .../plugins/plugin-auth/authorized/index.tsx | 2 +- .../plugins/plugin-auth/authorized/item.tsx | 2 +- .../plugin-auth/plugin-auth-in-agent.tsx | 2 +- .../plugins/plugin-auth/plugin-auth.tsx | 2 +- .../app-selector/app-inputs-panel.tsx | 2 +- .../app-selector/app-trigger.tsx | 2 +- .../plugin-detail-panel/detail-header.tsx | 2 +- .../plugin-detail-panel/endpoint-list.tsx | 2 +- .../plugin-detail-panel/endpoint-modal.tsx | 2 +- .../plugins/plugin-detail-panel/index.tsx | 2 +- .../model-selector/index.tsx | 2 +- .../model-selector/llm-params-panel.tsx | 2 +- .../model-selector/tts-params-panel.tsx | 2 +- .../multiple-tool-selector/index.tsx | 2 +- .../operation-dropdown.tsx | 2 +- .../plugin-detail-panel/strategy-detail.tsx | 2 +- .../plugin-detail-panel/strategy-item.tsx | 2 +- .../subscription-list/create/index.tsx | 2 +- .../subscription-list/list-view.tsx | 2 +- .../subscription-list/log-viewer.tsx | 2 +- .../subscription-list/selector-entry.tsx | 2 +- .../subscription-list/selector-view.tsx | 2 +- .../subscription-list/subscription-card.tsx | 2 +- .../tool-selector/index.tsx | 2 +- .../tool-selector/reasoning-config-form.tsx | 2 +- .../tool-selector/tool-credentials-form.tsx | 2 +- .../tool-selector/tool-item.tsx | 2 +- .../tool-selector/tool-trigger.tsx | 2 +- .../trigger/event-detail-drawer.tsx | 2 +- .../trigger/event-list.tsx | 2 +- .../components/plugins/plugin-item/index.tsx | 2 +- .../filter-management/category-filter.tsx | 2 +- .../filter-management/tag-filter.tsx | 2 +- .../components/plugins/plugin-page/index.tsx | 2 +- .../plugin-page/install-plugin-dropdown.tsx | 2 +- .../plugin-page/plugin-tasks/index.tsx | 2 +- web/app/components/plugins/provider-card.tsx | 2 +- .../plugins/readme-panel/entrance.tsx | 2 +- .../components/plugins/readme-panel/index.tsx | 2 +- .../auto-update-setting/index.tsx | 2 +- .../no-data-placeholder.tsx | 2 +- .../auto-update-setting/plugins-selected.tsx | 2 +- .../auto-update-setting/tool-picker.tsx | 2 +- .../plugins/reference-setting-modal/label.tsx | 2 +- .../update-plugin/from-market-place.tsx | 2 +- .../update-plugin/plugin-version-picker.tsx | 2 +- .../components/chunk-card-list/index.tsx | 2 +- .../panel/input-field/editor/index.tsx | 2 +- .../input-field/field-list/field-item.tsx | 2 +- .../field-list/field-list-container.tsx | 2 +- .../panel/input-field/field-list/index.tsx | 2 +- .../components/panel/input-field/index.tsx | 2 +- .../panel/input-field/preview/index.tsx | 2 +- .../data-source-options/option-card.tsx | 2 +- .../test-run/preparation/step-indicator.tsx | 2 +- .../panel/test-run/result/tabs/tab.tsx | 2 +- .../rag-pipeline-header/publisher/popup.tsx | 8 ++-- .../rag-pipeline-header/run-mode.tsx | 2 +- .../share/text-generation/index.tsx | 2 +- .../share/text-generation/info-modal.tsx | 2 +- .../share/text-generation/menu-dropdown.tsx | 2 +- .../run-batch/csv-reader/index.tsx | 2 +- .../share/text-generation/run-batch/index.tsx | 2 +- .../run-batch/res-download/index.tsx | 2 +- .../share/text-generation/run-once/index.tsx | 2 +- .../config-credentials.tsx | 2 +- .../edit-custom-collection-modal/index.tsx | 2 +- web/app/components/tools/labels/filter.tsx | 2 +- web/app/components/tools/labels/selector.tsx | 2 +- .../components/tools/mcp/detail/content.tsx | 2 +- .../tools/mcp/detail/list-loading.tsx | 2 +- .../tools/mcp/detail/operation-dropdown.tsx | 2 +- .../tools/mcp/detail/provider-detail.tsx | 2 +- .../components/tools/mcp/detail/tool-item.tsx | 2 +- .../components/tools/mcp/headers-input.tsx | 2 +- web/app/components/tools/mcp/index.tsx | 2 +- .../components/tools/mcp/mcp-server-modal.tsx | 2 +- .../components/tools/mcp/mcp-service-card.tsx | 2 +- web/app/components/tools/mcp/modal.tsx | 2 +- .../components/tools/mcp/provider-card.tsx | 2 +- web/app/components/tools/provider-list.tsx | 2 +- web/app/components/tools/provider/detail.tsx | 2 +- web/app/components/tools/provider/empty.tsx | 2 +- .../components/tools/provider/tool-item.tsx | 2 +- .../setting/build-in/config-credentials.tsx | 2 +- .../tools/workflow-tool/configure-button.tsx | 2 +- .../workflow-tool/confirm-modal/index.tsx | 2 +- .../components/tools/workflow-tool/index.tsx | 2 +- .../tools/workflow-tool/method-selector.tsx | 2 +- .../workflow-header/features-trigger.tsx | 2 +- .../start-node-option.tsx | 2 +- web/app/components/workflow/block-icon.tsx | 2 +- .../block-selector/all-start-blocks.tsx | 2 +- .../workflow/block-selector/all-tools.tsx | 2 +- .../workflow/block-selector/data-sources.tsx | 2 +- .../workflow/block-selector/index-bar.tsx | 6 +-- .../market-place-plugin/action.tsx | 2 +- .../market-place-plugin/item.tsx | 2 +- .../market-place-plugin/list.tsx | 2 +- .../rag-tool-recommendations/list.tsx | 2 +- .../workflow/block-selector/tabs.tsx | 2 +- .../workflow/block-selector/tool-picker.tsx | 2 +- .../block-selector/tool/action-item.tsx | 2 +- .../workflow/block-selector/tool/tool.tsx | 2 +- .../workflow/block-selector/tools.tsx | 4 +- .../trigger-plugin/action-item.tsx | 2 +- .../block-selector/trigger-plugin/item.tsx | 2 +- .../block-selector/view-type-select.tsx | 2 +- web/app/components/workflow/custom-edge.tsx | 2 +- .../workflow/dsl-export-confirm-modal.tsx | 2 +- .../workflow/header/chat-variable-button.tsx | 2 +- .../components/workflow/header/checklist.tsx | 2 +- .../components/workflow/header/env-button.tsx | 2 +- .../header/global-variable-button.tsx | 2 +- .../workflow/header/header-in-restoring.tsx | 2 +- .../workflow/header/run-and-history.tsx | 2 +- .../components/workflow/header/run-mode.tsx | 2 +- .../header/scroll-to-selected-node-button.tsx | 2 +- .../components/workflow/header/undo-redo.tsx | 9 ++-- .../header/version-history-button.tsx | 2 +- .../workflow/header/view-history.tsx | 2 +- .../workflow/header/view-workflow-history.tsx | 8 ++-- web/app/components/workflow/index.tsx | 2 +- .../nodes/_base/components/add-button.tsx | 2 +- .../components/agent-strategy-selector.tsx | 4 +- .../components/before-run-form/form-item.tsx | 2 +- .../_base/components/before-run-form/form.tsx | 2 +- .../components/before-run-form/index.tsx | 2 +- .../components/code-generator-button.tsx | 2 +- .../nodes/_base/components/collapse/index.tsx | 2 +- .../nodes/_base/components/editor/base.tsx | 2 +- .../code-editor/editor-support-vars.tsx | 2 +- .../components/editor/code-editor/index.tsx | 2 +- .../error-handle/error-handle-on-node.tsx | 2 +- .../workflow/nodes/_base/components/field.tsx | 2 +- .../nodes/_base/components/file-type-item.tsx | 2 +- .../_base/components/form-input-boolean.tsx | 2 +- .../_base/components/form-input-item.tsx | 2 +- .../components/form-input-type-switch.tsx | 2 +- .../workflow/nodes/_base/components/group.tsx | 6 +-- .../components/input-support-select-var.tsx | 2 +- .../components/install-plugin-button.tsx | 4 +- .../nodes/_base/components/layout/box.tsx | 2 +- .../_base/components/layout/field-title.tsx | 2 +- .../nodes/_base/components/layout/group.tsx | 2 +- .../nodes/_base/components/memory-config.tsx | 2 +- .../mixed-variable-text-input/index.tsx | 2 +- .../_base/components/next-step/container.tsx | 2 +- .../nodes/_base/components/next-step/item.tsx | 2 +- .../nodes/_base/components/node-handle.tsx | 2 +- .../nodes/_base/components/node-resizer.tsx | 2 +- .../_base/components/node-status-icon.tsx | 2 +- .../nodes/_base/components/option-card.tsx | 2 +- .../nodes/_base/components/output-vars.tsx | 2 +- .../nodes/_base/components/prompt/editor.tsx | 2 +- .../readonly-input-with-select-var.tsx | 2 +- .../_base/components/retry/retry-on-node.tsx | 2 +- .../nodes/_base/components/selector.tsx | 2 +- .../nodes/_base/components/setting-item.tsx | 4 +- .../workflow/nodes/_base/components/split.tsx | 2 +- .../components/support-var-input/index.tsx | 2 +- .../components/switch-plugin-version.tsx | 2 +- .../object-child-tree-panel/picker/field.tsx | 2 +- .../object-child-tree-panel/picker/index.tsx | 2 +- .../object-child-tree-panel/show/field.tsx | 2 +- .../tree-indent-line.tsx | 2 +- .../_base/components/variable/var-list.tsx | 2 +- .../variable/var-reference-picker.tsx | 2 +- .../variable/var-reference-vars.tsx | 2 +- .../components/variable/var-type-picker.tsx | 2 +- .../variable-label/base/variable-icon.tsx | 2 +- .../variable-label/base/variable-label.tsx | 2 +- .../variable-label/base/variable-name.tsx | 2 +- .../variable-icon-with-color.tsx | 2 +- .../variable-label-in-editor.tsx | 2 +- .../variable-label/variable-label-in-node.tsx | 2 +- .../variable-label/variable-label-in-text.tsx | 2 +- .../_base/components/workflow-panel/index.tsx | 2 +- .../workflow-panel/trigger-subscription.tsx | 2 +- .../components/workflow/nodes/_base/node.tsx | 2 +- .../nodes/agent/components/tool-icon.tsx | 18 +++----- .../components/operation-selector.tsx | 14 ++---- .../nodes/data-source-empty/index.tsx | 2 +- .../nodes/http/components/api-input.tsx | 2 +- .../http/components/authorization/index.tsx | 2 +- .../components/authorization/radio-group.tsx | 2 +- .../nodes/http/components/edit-body/index.tsx | 2 +- .../key-value/key-value-edit/index.tsx | 2 +- .../key-value/key-value-edit/input-item.tsx | 2 +- .../key-value/key-value-edit/item.tsx | 2 +- .../components/workflow/nodes/http/panel.tsx | 2 +- .../condition-list/condition-item.tsx | 2 +- .../condition-list/condition-operator.tsx | 2 +- .../components/condition-list/index.tsx | 2 +- .../components/condition-number-input.tsx | 2 +- .../if-else/components/condition-wrap.tsx | 2 +- .../workflow/nodes/iteration/add-block.tsx | 2 +- .../workflow/nodes/iteration/node.tsx | 2 +- .../components/chunk-structure/hooks.tsx | 2 +- .../chunk-structure/instruction/index.tsx | 2 +- .../components/index-method.tsx | 2 +- .../knowledge-base/components/option-card.tsx | 2 +- .../search-method-option.tsx | 2 +- .../condition-list/condition-date.tsx | 2 +- .../condition-list/condition-item.tsx | 2 +- .../condition-list/condition-operator.tsx | 2 +- .../condition-list/condition-value-method.tsx | 2 +- .../metadata/condition-list/index.tsx | 2 +- .../components/metadata/metadata-icon.tsx | 2 +- .../components/retrieval-config.tsx | 2 +- .../components/extract-input.tsx | 2 +- .../components/filter-condition.tsx | 2 +- .../list-operator/components/limit-config.tsx | 2 +- .../components/sub-variable-picker.tsx | 2 +- .../nodes/llm/components/config-prompt.tsx | 2 +- .../json-schema-config-modal/code-editor.tsx | 6 +-- .../error-message.tsx | 8 ++-- .../json-importer.tsx | 2 +- .../json-schema-generator/index.tsx | 2 +- .../schema-editor.tsx | 2 +- .../edit-card/auto-width-input.tsx | 2 +- .../visual-editor/edit-card/index.tsx | 4 +- .../visual-editor/edit-card/type-selector.tsx | 2 +- .../visual-editor/index.tsx | 2 +- .../visual-editor/schema-node.tsx | 18 +++----- .../llm/components/prompt-generator-btn.tsx | 2 +- .../nodes/llm/components/structure-output.tsx | 2 +- .../workflow/nodes/loop/add-block.tsx | 2 +- .../condition-list/condition-item.tsx | 2 +- .../condition-list/condition-operator.tsx | 2 +- .../loop/components/condition-list/index.tsx | 2 +- .../components/condition-number-input.tsx | 2 +- .../nodes/loop/components/condition-wrap.tsx | 2 +- .../workflow/nodes/loop/insert-block.tsx | 2 +- .../components/workflow/nodes/loop/node.tsx | 2 +- .../extract-parameter/import-from-tool.tsx | 2 +- .../components/class-list.tsx | 2 +- .../nodes/start/components/var-item.tsx | 2 +- .../nodes/start/components/var-list.tsx | 2 +- .../nodes/tool/components/input-var-list.tsx | 2 +- .../mixed-variable-text-input/index.tsx | 2 +- .../components/generic-table.tsx | 2 +- .../components/paragraph-input.tsx | 2 +- .../components/add-variable/index.tsx | 2 +- .../components/node-group-item.tsx | 2 +- .../components/node-variable-item.tsx | 2 +- .../nodes/variable-assigner/panel.tsx | 2 +- .../components/workflow/note-node/index.tsx | 2 +- .../plugins/link-editor-plugin/component.tsx | 2 +- .../note-editor/toolbar/color-picker.tsx | 2 +- .../note-node/note-editor/toolbar/command.tsx | 2 +- .../toolbar/font-size-selector.tsx | 2 +- .../note-editor/toolbar/operator.tsx | 2 +- .../workflow/operator/add-block.tsx | 2 +- .../components/workflow/operator/control.tsx | 2 +- .../workflow/operator/more-actions.tsx | 2 +- .../workflow/operator/zoom-in-out.tsx | 2 +- .../components/workflow/panel-contextmenu.tsx | 2 +- .../components/array-bool-list.tsx | 2 +- .../components/variable-item.tsx | 2 +- .../components/variable-modal.tsx | 2 +- .../components/variable-type-select.tsx | 2 +- .../panel/chat-variable-panel/index.tsx | 2 +- .../conversation-variable-modal.tsx | 2 +- .../panel/debug-and-preview/index.tsx | 2 +- .../panel/debug-and-preview/user-input.tsx | 2 +- .../workflow/panel/env-panel/env-item.tsx | 2 +- .../workflow/panel/env-panel/index.tsx | 2 +- .../panel/env-panel/variable-modal.tsx | 2 +- .../panel/global-variable-panel/index.tsx | 2 +- .../panel/global-variable-panel/item.tsx | 2 +- web/app/components/workflow/panel/index.tsx | 2 +- .../context-menu/menu-item.tsx | 2 +- .../version-history-panel/filter/index.tsx | 2 +- .../version-history-panel/loading/item.tsx | 2 +- .../version-history-item.tsx | 2 +- .../workflow/panel/workflow-preview.tsx | 2 +- .../workflow/run/agent-log/agent-log-item.tsx | 2 +- web/app/components/workflow/run/index.tsx | 2 +- .../iteration-log/iteration-result-panel.tsx | 2 +- .../run/loop-log/loop-result-panel.tsx | 2 +- .../workflow/run/loop-result-panel.tsx | 2 +- web/app/components/workflow/run/node.tsx | 2 +- .../workflow/run/status-container.tsx | 2 +- web/app/components/workflow/run/status.tsx | 2 +- .../components/workflow/run/tracing-panel.tsx | 2 +- .../components/workflow/shortcuts-name.tsx | 2 +- .../components/workflow/simple-node/index.tsx | 2 +- .../variable-inspect/display-content.tsx | 2 +- .../workflow/variable-inspect/group.tsx | 2 +- .../workflow/variable-inspect/index.tsx | 2 +- .../variable-inspect/large-data-alert.tsx | 2 +- .../workflow/variable-inspect/left.tsx | 2 +- .../workflow/variable-inspect/panel.tsx | 2 +- .../workflow/variable-inspect/right.tsx | 2 +- .../workflow/variable-inspect/trigger.tsx | 2 +- .../variable-inspect/value-content.tsx | 2 +- .../components/error-handle-on-node.tsx | 2 +- .../components/node-handle.tsx | 2 +- .../components/nodes/base.tsx | 2 +- .../components/nodes/iteration/node.tsx | 2 +- .../components/nodes/loop/node.tsx | 2 +- .../components/note-node/index.tsx | 2 +- .../components/zoom-in-out.tsx | 2 +- .../workflow/workflow-preview/index.tsx | 2 +- web/app/education-apply/role-selector.tsx | 2 +- .../forgot-password/ChangePasswordForm.tsx | 2 +- web/app/forgot-password/page.tsx | 2 +- web/app/init/page.tsx | 2 +- web/app/install/installForm.tsx | 4 +- web/app/install/page.tsx | 2 +- web/app/layout.tsx | 2 +- web/app/reset-password/layout.tsx | 2 +- web/app/reset-password/set-password/page.tsx | 2 +- web/app/signin/components/social-auth.tsx | 14 ++---- web/app/signin/layout.tsx | 2 +- web/app/signin/normal-form.tsx | 2 +- web/app/signin/split.tsx | 2 +- web/app/signup/layout.tsx | 2 +- web/app/signup/set-password/page.tsx | 2 +- web/package.json | 2 +- web/pnpm-lock.yaml | 11 ++--- web/utils/classnames.spec.ts | 2 +- web/utils/classnames.ts | 8 ++-- 815 files changed, 1064 insertions(+), 1227 deletions(-) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index 1f836de6e6..d5e3c61932 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -16,7 +16,7 @@ import { import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' import s from './style.module.css' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useStore } from '@/app/components/app/store' import AppSideBar from '@/app/components/app-sidebar' import type { NavIcon } from '@/app/components/app-sidebar/navLink' diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx index 2bfdece433..dda5dff2b9 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx @@ -3,7 +3,7 @@ import { RiCalendarLine } from '@remixicon/react' import type { Dayjs } from 'dayjs' import type { FC } from 'react' import React, { useCallback } from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { formatToLocalTime } from '@/utils/format' import { useI18N } from '@/context/i18n' import Picker from '@/app/components/base/date-and-time-picker/date-picker' diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx index f99ea52492..0a80bf670d 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx @@ -6,7 +6,7 @@ import { SimpleSelect } from '@/app/components/base/select' import type { Item } from '@/app/components/base/select' import dayjs from 'dayjs' import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useTranslation } from 'react-i18next' const today = dayjs() diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx index 246a1eb6a3..17c919bf22 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useRef, useState } from 'react' import type { PopupProps } from './config-popup' import ConfigPopup from './config-popup' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { PortalToFollowElem, PortalToFollowElemContent, diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx index 628eb13071..767ccb8c59 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx @@ -12,7 +12,7 @@ import Indicator from '@/app/components/header/indicator' import Switch from '@/app/components/base/switch' import Tooltip from '@/app/components/base/tooltip' import Divider from '@/app/components/base/divider' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' const I18N_PREFIX = 'app.tracing' diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx index eecd356e08..e170159e35 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import React from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Input from '@/app/components/base/input' type Props = { diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 2c17931b83..319ff3f423 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -12,7 +12,7 @@ import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangS import { TracingProvider } from './type' import TracingIcon from './tracing-icon' import ConfigButton from './config-button' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing' import Indicator from '@/app/components/header/indicator' import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps' diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx index ac1704d60d..0779689c76 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx @@ -6,7 +6,7 @@ import { } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { TracingProvider } from './type' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { AliyunIconBig, ArizeIconBig, DatabricksIconBig, LangfuseIconBig, LangsmithIconBig, MlflowIconBig, OpikIconBig, PhoenixIconBig, TencentIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing' import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general' diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx index ec9117dd38..aeca1cd3ab 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import React from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing' type Props = { diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 3effb79f20..3581587b54 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -23,7 +23,7 @@ import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use import useDocumentTitle from '@/hooks/use-document-title' import ExtraInfo from '@/app/components/datasets/extra-info' import { useEventEmitterContextContext } from '@/context/event-emitter' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' export type IAppDetailLayoutProps = { children: React.ReactNode diff --git a/web/app/(shareLayout)/webapp-reset-password/layout.tsx b/web/app/(shareLayout)/webapp-reset-password/layout.tsx index e0ac6b9ad6..13073b0e6a 100644 --- a/web/app/(shareLayout)/webapp-reset-password/layout.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/layout.tsx @@ -1,7 +1,7 @@ 'use client' import Header from '@/app/signin/_header' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useGlobalPublicStore } from '@/context/global-public-context' export default function SignInLayout({ children }: any) { diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx index 5e3f6fff1d..843f10e039 100644 --- a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRouter, useSearchParams } from 'next/navigation' -import cn from 'classnames' +import { cn } from '@/utils/classnames' import { RiCheckboxCircleFill } from '@remixicon/react' import { useCountDown } from 'ahooks' import Button from '@/app/components/base/button' diff --git a/web/app/(shareLayout)/webapp-signin/layout.tsx b/web/app/(shareLayout)/webapp-signin/layout.tsx index 7649982072..c75f925d40 100644 --- a/web/app/(shareLayout)/webapp-signin/layout.tsx +++ b/web/app/(shareLayout)/webapp-signin/layout.tsx @@ -1,6 +1,6 @@ 'use client' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' import type { PropsWithChildren } from 'react' diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx index 219722eef3..a14bfcd737 100644 --- a/web/app/(shareLayout)/webapp-signin/normalForm.tsx +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -7,7 +7,7 @@ import Loading from '@/app/components/base/loading' import MailAndCodeAuth from './components/mail-and-code-auth' import MailAndPasswordAuth from './components/mail-and-password-auth' import SSOAuth from './components/sso-auth' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { LicenseStatus } from '@/types/feature' import { IS_CE_EDITION } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' diff --git a/web/app/account/oauth/authorize/layout.tsx b/web/app/account/oauth/authorize/layout.tsx index 2ab676d6b6..b70ab210d0 100644 --- a/web/app/account/oauth/authorize/layout.tsx +++ b/web/app/account/oauth/authorize/layout.tsx @@ -1,7 +1,7 @@ 'use client' import Header from '@/app/signin/_header' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' import { AppContextProvider } from '@/context/app-context' diff --git a/web/app/activate/activateForm.tsx b/web/app/activate/activateForm.tsx index d9d07cbfa1..4789a579a7 100644 --- a/web/app/activate/activateForm.tsx +++ b/web/app/activate/activateForm.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next' import useSWR from 'swr' import { useRouter, useSearchParams } from 'next/navigation' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Button from '@/app/components/base/button' import { invitationCheck } from '@/service/common' diff --git a/web/app/activate/page.tsx b/web/app/activate/page.tsx index cfb1e6b149..9ae03f3711 100644 --- a/web/app/activate/page.tsx +++ b/web/app/activate/page.tsx @@ -2,7 +2,7 @@ import React from 'react' import Header from '../signin/_header' import ActivateForm from './activateForm' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useGlobalPublicStore } from '@/context/global-public-context' const Activate = () => { diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index f143c2fcef..1b4377c10a 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -29,7 +29,7 @@ import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overvie import type { Operation } from './app-operations' import AppOperations from './app-operations' import dynamic from 'next/dynamic' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { AppModeEnum } from '@/types/app' const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), { diff --git a/web/app/components/app-sidebar/app-sidebar-dropdown.tsx b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx index 3c5d38dd82..04634906af 100644 --- a/web/app/components/app-sidebar/app-sidebar-dropdown.tsx +++ b/web/app/components/app-sidebar/app-sidebar-dropdown.tsx @@ -16,7 +16,7 @@ import AppInfo from './app-info' import NavLink from './navLink' import { useStore as useAppStore } from '@/app/components/app/store' import type { NavIcon } from './navLink' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { AppModeEnum } from '@/types/app' type Props = { diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index ff110f70bd..dc46af2d02 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useState } from 'react' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem' import ActionButton from '../../base/action-button' import { RiMoreFill } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Menu from './menu' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' diff --git a/web/app/components/app-sidebar/dataset-info/index.tsx b/web/app/components/app-sidebar/dataset-info/index.tsx index 44b0baa72b..bace656d54 100644 --- a/web/app/components/app-sidebar/dataset-info/index.tsx +++ b/web/app/components/app-sidebar/dataset-info/index.tsx @@ -8,7 +8,7 @@ import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import type { DataSet } from '@/models/datasets' import { DOC_FORM_TEXT } from '@/models/datasets' import { useKnowledge } from '@/hooks/use-knowledge' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Dropdown from './dropdown' type DatasetInfoProps = { diff --git a/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx b/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx index ac07333712..cf380d00d2 100644 --- a/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx @@ -11,7 +11,7 @@ import AppIcon from '../base/app-icon' import Divider from '../base/divider' import NavLink from './navLink' import type { NavIcon } from './navLink' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import Effect from '../base/effect' import Dropdown from './dataset-info/dropdown' diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index 86de2e2034..fe52c4cfa2 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -9,7 +9,7 @@ import AppSidebarDropdown from './app-sidebar-dropdown' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useStore as useAppStore } from '@/app/components/app/store' import { useEventEmitterContextContext } from '@/context/event-emitter' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Divider from '../base/divider' import { useHover, useKeyPress } from 'ahooks' import ToggleButton from './toggle-button' diff --git a/web/app/components/app-sidebar/navLink.tsx b/web/app/components/app-sidebar/navLink.tsx index ad90b91250..f6d8e57682 100644 --- a/web/app/components/app-sidebar/navLink.tsx +++ b/web/app/components/app-sidebar/navLink.tsx @@ -2,7 +2,7 @@ import React from 'react' import { useSelectedLayoutSegment } from 'next/navigation' import Link from 'next/link' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' import type { RemixiconComponentType } from '@remixicon/react' export type NavIcon = React.ComponentType< @@ -42,7 +42,7 @@ const NavLink = ({ const NavIcon = isActive ? iconMap.selected : iconMap.normal const renderIcon = () => ( -
+
) @@ -53,21 +53,17 @@ const NavLink = ({ key={name} type='button' disabled - className={classNames( - 'system-sm-medium flex h-8 cursor-not-allowed items-center rounded-lg text-components-menu-item-text opacity-30 hover:bg-components-menu-item-bg-hover', - 'pl-3 pr-1', - )} + className={cn('system-sm-medium flex h-8 cursor-not-allowed items-center rounded-lg text-components-menu-item-text opacity-30 hover:bg-components-menu-item-bg-hover', + 'pl-3 pr-1')} title={mode === 'collapse' ? name : ''} aria-disabled > {renderIcon()} {name} @@ -79,22 +75,18 @@ const NavLink = ({ {renderIcon()} {name} diff --git a/web/app/components/app-sidebar/toggle-button.tsx b/web/app/components/app-sidebar/toggle-button.tsx index 8de6f887f6..4f69adfc34 100644 --- a/web/app/components/app-sidebar/toggle-button.tsx +++ b/web/app/components/app-sidebar/toggle-button.tsx @@ -1,7 +1,7 @@ import React from 'react' import Button from '../base/button' import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Tooltip from '../base/tooltip' import { useTranslation } from 'react-i18next' import { getKeyboardKeyNameBySystem } from '../workflow/utils' diff --git a/web/app/components/app/annotation/batch-action.tsx b/web/app/components/app/annotation/batch-action.tsx index 6e80d0c4c8..6ff392d17e 100644 --- a/web/app/components/app/annotation/batch-action.tsx +++ b/web/app/components/app/annotation/batch-action.tsx @@ -3,7 +3,7 @@ import { RiDeleteBinLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' import { useBoolean } from 'ahooks' import Divider from '@/app/components/base/divider' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Confirm from '@/app/components/base/confirm' const i18nPrefix = 'appAnnotation.batchAction' @@ -38,7 +38,7 @@ const BatchAction: FC = ({ setIsNotDeleting() } return ( -
+
diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx index ccad46b860..c9766135df 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import { RiDeleteBinLine } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' import { ToastContext } from '@/app/components/base/toast' import Button from '@/app/components/base/button' diff --git a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx index 37b5ab0686..6ba830967d 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx @@ -6,7 +6,7 @@ import { RiDeleteBinLine, RiEditFill, RiEditLine } from '@remixicon/react' import { Robot, User } from '@/app/components/base/icons/src/public/avatar' import Textarea from '@/app/components/base/textarea' import Button from '@/app/components/base/button' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' export enum EditItemType { Query = 'query', diff --git a/web/app/components/app/annotation/header-opts/index.tsx b/web/app/components/app/annotation/header-opts/index.tsx index 024f75867c..5f8ef658e7 100644 --- a/web/app/components/app/annotation/header-opts/index.tsx +++ b/web/app/components/app/annotation/header-opts/index.tsx @@ -17,7 +17,7 @@ import Button from '../../../base/button' import AddAnnotationModal from '../add-annotation-modal' import type { AnnotationItemBasic } from '../type' import BatchAddModal from '../batch-add-annotation-modal' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import CustomPopover from '@/app/components/base/popover' import { FileDownload02, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files' import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows' diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index 32d0c799fc..2d639c91e4 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -25,7 +25,7 @@ import { sleep } from '@/utils' import { useProviderContext } from '@/context/provider-context' import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' import { type App, AppModeEnum } from '@/types/app' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { delAnnotations } from '@/service/annotation' type Props = { diff --git a/web/app/components/app/annotation/list.tsx b/web/app/components/app/annotation/list.tsx index 4135b4362e..62a0c50e60 100644 --- a/web/app/components/app/annotation/list.tsx +++ b/web/app/components/app/annotation/list.tsx @@ -7,7 +7,7 @@ import type { AnnotationItem } from './type' import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal' import ActionButton from '@/app/components/base/action-button' import useTimestamp from '@/hooks/use-timestamp' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Checkbox from '@/app/components/base/checkbox' import BatchAction from './batch-action' diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx index 8426ab0005..d21b177098 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx @@ -14,7 +14,7 @@ import TabSlider from '@/app/components/base/tab-slider-plain' import { fetchHitHistoryList } from '@/service/annotation' import { APP_PAGE_LIMIT } from '@/config' import useTimestamp from '@/hooks/use-timestamp' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' type Props = { appId: string diff --git a/web/app/components/app/app-access-control/access-control-dialog.tsx b/web/app/components/app/app-access-control/access-control-dialog.tsx index ee3fa9650b..99cf6d7074 100644 --- a/web/app/components/app/app-access-control/access-control-dialog.tsx +++ b/web/app/components/app/app-access-control/access-control-dialog.tsx @@ -2,7 +2,7 @@ import { Fragment, useCallback } from 'react' import type { ReactNode } from 'react' import { Dialog, Transition } from '@headlessui/react' import { RiCloseLine } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' type DialogProps = { className?: string diff --git a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx index bb8dabbae6..17263fdd46 100644 --- a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx +++ b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx @@ -11,7 +11,7 @@ import Input from '../../base/input' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem' import Loading from '../../base/loading' import useAccessControlStore from '../../../../context/access-control-store' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useSearchForWhiteListCandidates } from '@/service/access-control' import type { AccessControlAccount, AccessControlGroup, Subject, SubjectAccount, SubjectGroup } from '@/models/access-control' import { SubjectType } from '@/models/access-control' @@ -106,7 +106,7 @@ function SelectedGroupsBreadCrumb() { setSelectedGroupsForBreadcrumb([]) }, [setSelectedGroupsForBreadcrumb]) return
- 0 && 'cursor-pointer text-text-accent')} onClick={handleReset}>{t('app.accessControlDialog.operateGroupAndMember.allMembers')} + 0 && 'cursor-pointer text-text-accent')} onClick={handleReset}>{t('app.accessControlDialog.operateGroupAndMember.allMembers')} {selectedGroupsForBreadcrumb.map((group, index) => { return
/ @@ -198,7 +198,7 @@ type BaseItemProps = { children: React.ReactNode } function BaseItem({ children, className }: BaseItemProps) { - return
+ return
{children}
} diff --git a/web/app/components/app/app-publisher/suggested-action.tsx b/web/app/components/app/app-publisher/suggested-action.tsx index 2535de6654..154bacc361 100644 --- a/web/app/components/app/app-publisher/suggested-action.tsx +++ b/web/app/components/app/app-publisher/suggested-action.tsx @@ -1,6 +1,6 @@ import type { HTMLProps, PropsWithChildren } from 'react' import { RiArrowRightUpLine } from '@remixicon/react' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' export type SuggestedActionProps = PropsWithChildren & { icon?: React.ReactNode @@ -19,11 +19,9 @@ const SuggestedAction = ({ icon, link, disabled, children, className, onClick, . href={disabled ? undefined : link} target='_blank' rel='noreferrer' - className={classNames( - 'flex items-center justify-start gap-2 rounded-lg bg-background-section-burn px-2.5 py-2 text-text-secondary transition-colors [&:not(:first-child)]:mt-1', + className={cn('flex items-center justify-start gap-2 rounded-lg bg-background-section-burn px-2.5 py-2 text-text-secondary transition-colors [&:not(:first-child)]:mt-1', disabled ? 'cursor-not-allowed opacity-30 shadow-xs' : 'cursor-pointer text-text-secondary hover:bg-state-accent-hover hover:text-text-accent', - className, - )} + className)} onClick={handleClick} {...props} > diff --git a/web/app/components/app/configuration/base/feature-panel/index.tsx b/web/app/components/app/configuration/base/feature-panel/index.tsx index ec5ab96d76..c9ebfefbe5 100644 --- a/web/app/components/app/configuration/base/feature-panel/index.tsx +++ b/web/app/components/app/configuration/base/feature-panel/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC, ReactNode } from 'react' import React from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' export type IFeaturePanelProps = { className?: string diff --git a/web/app/components/app/configuration/base/operation-btn/index.tsx b/web/app/components/app/configuration/base/operation-btn/index.tsx index aba35cded2..db19d2976e 100644 --- a/web/app/components/app/configuration/base/operation-btn/index.tsx +++ b/web/app/components/app/configuration/base/operation-btn/index.tsx @@ -6,7 +6,7 @@ import { RiAddLine, RiEditLine, } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { noop } from 'lodash-es' export type IOperationBtnProps = { diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx index 5bf2f177ff..6492864ce2 100644 --- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx @@ -14,7 +14,7 @@ import s from './style.module.css' import MessageTypeSelector from './message-type-selector' import ConfirmAddVar from './confirm-add-var' import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import type { PromptRole, PromptVariable } from '@/models/debug' import { Copy, diff --git a/web/app/components/app/configuration/config-prompt/message-type-selector.tsx b/web/app/components/app/configuration/config-prompt/message-type-selector.tsx index 17b3ecb2f1..71f3e6ee5f 100644 --- a/web/app/components/app/configuration/config-prompt/message-type-selector.tsx +++ b/web/app/components/app/configuration/config-prompt/message-type-selector.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import React from 'react' import { useBoolean, useClickAway } from 'ahooks' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { PromptRole } from '@/models/debug' import { ChevronSelectorVertical } from '@/app/components/base/icons/src/vender/line/arrows' type Props = { diff --git a/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.tsx b/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.tsx index 9e10db93ae..90a19c883a 100644 --- a/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.tsx +++ b/web/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react' import type { FC } from 'react' import { useDebounceFn } from 'ahooks' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' type Props = { className?: string diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index 68bf6dd7c2..e4c21b0cbc 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -7,7 +7,7 @@ import { produce } from 'immer' import { useContext } from 'use-context-selector' import ConfirmAddVar from './confirm-add-var' import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import type { PromptVariable } from '@/models/debug' import Tooltip from '@/app/components/base/tooltip' import { AppModeEnum } from '@/types/app' diff --git a/web/app/components/app/configuration/config-var/config-modal/field.tsx b/web/app/components/app/configuration/config-var/config-modal/field.tsx index b24e0be6ce..76d228358a 100644 --- a/web/app/components/app/configuration/config-var/config-modal/field.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/field.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import React from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useTranslation } from 'react-i18next' type Props = { diff --git a/web/app/components/app/configuration/config-var/config-modal/type-select.tsx b/web/app/components/app/configuration/config-var/config-modal/type-select.tsx index 2b52991d4a..53d59eb24b 100644 --- a/web/app/components/app/configuration/config-var/config-modal/type-select.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/type-select.tsx @@ -2,7 +2,6 @@ import type { FC } from 'react' import React, { useState } from 'react' import { ChevronDownIcon } from '@heroicons/react/20/solid' -import classNames from '@/utils/classnames' import { PortalToFollowElem, PortalToFollowElemContent, @@ -10,7 +9,7 @@ import { } from '@/app/components/base/portal-to-follow-elem' import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon' import type { InputVarType } from '@/app/components/workflow/types' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Badge from '@/app/components/base/badge' import { inputVarTypeToVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils' @@ -47,7 +46,7 @@ const TypeSelector: FC = ({ > !readonly && setOpen(v => !v)} className='w-full'>
@@ -69,7 +68,7 @@ const TypeSelector: FC = ({
{items.map((item: Item) => (
{ const { t } = useTranslation() diff --git a/web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx b/web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx index 6512e11545..6193392026 100644 --- a/web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx +++ b/web/app/components/app/configuration/config/agent/agent-setting/item-panel.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import React from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Tooltip from '@/app/components/base/tooltip' type Props = { className?: string diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index 4793b5fe49..8dfa2f194b 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -25,7 +25,7 @@ import { MAX_TOOLS_NUM } from '@/config' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import Tooltip from '@/app/components/base/tooltip' import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types' import { canFindTool } from '@/utils' diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index c5947495db..0627666b4c 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -22,7 +22,7 @@ import { CollectionType } from '@/app/components/tools/types' import { fetchBuiltInToolList, fetchCustomToolList, fetchModelToolList, fetchWorkflowToolList } from '@/service/tools' import I18n from '@/context/i18n' import { getLanguage } from '@/i18n-config/language' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import type { ToolWithProvider } from '@/app/components/workflow/types' import { AuthCategory, diff --git a/web/app/components/app/configuration/config/agent/prompt-editor.tsx b/web/app/components/app/configuration/config/agent/prompt-editor.tsx index 71a9304d0c..78d7eef029 100644 --- a/web/app/components/app/configuration/config/agent/prompt-editor.tsx +++ b/web/app/components/app/configuration/config/agent/prompt-editor.tsx @@ -4,7 +4,7 @@ import React from 'react' import copy from 'copy-to-clipboard' import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { Copy, CopyCheck, diff --git a/web/app/components/app/configuration/config/assistant-type-picker/index.tsx b/web/app/components/app/configuration/config/assistant-type-picker/index.tsx index 3597a6e292..50f16f957a 100644 --- a/web/app/components/app/configuration/config/assistant-type-picker/index.tsx +++ b/web/app/components/app/configuration/config/assistant-type-picker/index.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { RiArrowDownSLine } from '@remixicon/react' import AgentSetting from '../agent/agent-setting' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { PortalToFollowElem, PortalToFollowElemContent, diff --git a/web/app/components/app/configuration/config/automatic/idea-output.tsx b/web/app/components/app/configuration/config/automatic/idea-output.tsx index df4f76c92b..895f74baa3 100644 --- a/web/app/components/app/configuration/config/automatic/idea-output.tsx +++ b/web/app/components/app/configuration/config/automatic/idea-output.tsx @@ -3,7 +3,7 @@ import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid import { useBoolean } from 'ahooks' import type { FC } from 'react' import React from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Textarea from '@/app/components/base/textarea' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/configuration/config/automatic/instruction-editor.tsx b/web/app/components/app/configuration/config/automatic/instruction-editor.tsx index b14ee93313..409f335232 100644 --- a/web/app/components/app/configuration/config/automatic/instruction-editor.tsx +++ b/web/app/components/app/configuration/config/automatic/instruction-editor.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import React from 'react' import PromptEditor from '@/app/components/base/prompt-editor' import type { GeneratorType } from './types' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/configuration/config/automatic/prompt-toast.tsx b/web/app/components/app/configuration/config/automatic/prompt-toast.tsx index 2826cc97c8..c9169f0ad7 100644 --- a/web/app/components/app/configuration/config/automatic/prompt-toast.tsx +++ b/web/app/components/app/configuration/config/automatic/prompt-toast.tsx @@ -1,7 +1,7 @@ import { RiArrowDownSLine, RiSparklingFill } from '@remixicon/react' import { useBoolean } from 'ahooks' import React from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { Markdown } from '@/app/components/base/markdown' import { useTranslation } from 'react-i18next' import s from './style.module.css' diff --git a/web/app/components/app/configuration/config/automatic/version-selector.tsx b/web/app/components/app/configuration/config/automatic/version-selector.tsx index c3d3e1d91c..715c1f3c80 100644 --- a/web/app/components/app/configuration/config/automatic/version-selector.tsx +++ b/web/app/components/app/configuration/config/automatic/version-selector.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' import { useBoolean } from 'ahooks' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/configuration/dataset-config/card-item/index.tsx b/web/app/components/app/configuration/dataset-config/card-item/index.tsx index 85d46122a3..7fd7011a56 100644 --- a/web/app/components/app/configuration/dataset-config/card-item/index.tsx +++ b/web/app/components/app/configuration/dataset-config/card-item/index.tsx @@ -13,7 +13,7 @@ import Drawer from '@/app/components/base/drawer' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import Badge from '@/app/components/base/badge' import { useKnowledge } from '@/hooks/use-knowledge' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import AppIcon from '@/app/components/base/app-icon' type ItemProps = { diff --git a/web/app/components/app/configuration/dataset-config/context-var/index.tsx b/web/app/components/app/configuration/dataset-config/context-var/index.tsx index ebba9c51cb..80cc50acdf 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/index.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/index.tsx @@ -4,7 +4,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import type { Props } from './var-picker' import VarPicker from './var-picker' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { BracketsX } from '@/app/components/base/icons/src/vender/line/development' import Tooltip from '@/app/components/base/tooltip' diff --git a/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx b/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx index c443ea0b1f..f5ea2eaa27 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { ChevronDownIcon } from '@heroicons/react/24/outline' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { PortalToFollowElem, PortalToFollowElemContent, diff --git a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx index 8e06d6c901..c7a43fbfbd 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx @@ -20,7 +20,7 @@ import type { DataSet, } from '@/models/datasets' import { RerankingModeEnum } from '@/models/datasets' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useSelectedDatasetsMode } from '@/app/components/workflow/nodes/knowledge-retrieval/hooks' import Switch from '@/app/components/base/switch' import Toast from '@/app/components/base/toast' diff --git a/web/app/components/app/configuration/dataset-config/params-config/index.tsx b/web/app/components/app/configuration/dataset-config/params-config/index.tsx index df2b4293c4..24da958217 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/index.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/index.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import { RiEqualizer2Line } from '@remixicon/react' import ConfigContent from './config-content' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import ConfigContext from '@/context/debug-configuration' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' diff --git a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx index ebfa3b1e12..459623104d 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx @@ -2,7 +2,7 @@ import { memo } from 'react' import { useTranslation } from 'react-i18next' import './weighted-score.css' import Slider from '@/app/components/base/slider' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { noop } from 'lodash-es' const formatNumber = (value: number) => { diff --git a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx index 6857c38e1e..f02fdcb5d7 100644 --- a/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx +++ b/web/app/components/app/configuration/dataset-config/select-dataset/index.tsx @@ -10,7 +10,7 @@ import Button from '@/app/components/base/button' import Loading from '@/app/components/base/loading' import Badge from '@/app/components/base/badge' import { useKnowledge } from '@/hooks/use-knowledge' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import AppIcon from '@/app/components/base/app-icon' import { useInfiniteDatasets } from '@/service/knowledge/use-dataset' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 37d9ddd372..8c3e753b22 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -4,7 +4,7 @@ import { useMount } from 'ahooks' import { useTranslation } from 'react-i18next' import { isEqual } from 'lodash-es' import { RiCloseLine } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import IndexMethod from '@/app/components/datasets/settings/index-method' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx index 5ea799d092..99d042f681 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/retrieval-section.tsx @@ -1,6 +1,6 @@ import { RiCloseLine } from '@remixicon/react' import type { FC } from 'react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Divider from '@/app/components/base/divider' import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' diff --git a/web/app/components/app/configuration/debug/chat-user-input.tsx b/web/app/components/app/configuration/debug/chat-user-input.tsx index 16666d514e..c25bed548c 100644 --- a/web/app/components/app/configuration/debug/chat-user-input.tsx +++ b/web/app/components/app/configuration/debug/chat-user-input.tsx @@ -7,7 +7,7 @@ import Select from '@/app/components/base/select' import Textarea from '@/app/components/base/textarea' import { DEFAULT_VALUE_MAX_LEN } from '@/config' import type { Inputs } from '@/models/debug' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input' type Props = { diff --git a/web/app/components/app/configuration/prompt-value-panel/index.tsx b/web/app/components/app/configuration/prompt-value-panel/index.tsx index 005f7f938f..9874664443 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -21,7 +21,7 @@ import FeatureBar from '@/app/components/base/features/new-feature-panel/feature import type { VisionFile, VisionSettings } from '@/types/app' import { DEFAULT_VALUE_MAX_LEN } from '@/config' import { useStore as useAppStore } from '@/app/components/app/store' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input' export type IPromptValuePanelProps = { diff --git a/web/app/components/app/create-app-dialog/app-card/index.tsx b/web/app/components/app/create-app-dialog/app-card/index.tsx index a3bf91cb5d..df35a74ec7 100644 --- a/web/app/components/app/create-app-dialog/app-card/index.tsx +++ b/web/app/components/app/create-app-dialog/app-card/index.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { PlusIcon } from '@heroicons/react/20/solid' import { AppTypeIcon, AppTypeLabel } from '../../type-selector' import Button from '@/app/components/base/button' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import type { App } from '@/models/explore' import AppIcon from '@/app/components/base/app-icon' diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index 51b6874d52..4655d7a676 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -11,7 +11,7 @@ import AppCard from '../app-card' import Sidebar, { AppCategories, AppCategoryLabel } from './sidebar' import Toast from '@/app/components/base/toast' import Divider from '@/app/components/base/divider' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import ExploreContext from '@/context/explore-context' import type { App } from '@/models/explore' import { fetchAppDetail, fetchAppList } from '@/service/explore' diff --git a/web/app/components/app/create-app-dialog/app-list/sidebar.tsx b/web/app/components/app/create-app-dialog/app-list/sidebar.tsx index 85c55c5385..89062cdcf9 100644 --- a/web/app/components/app/create-app-dialog/app-list/sidebar.tsx +++ b/web/app/components/app/create-app-dialog/app-list/sidebar.tsx @@ -1,7 +1,7 @@ 'use client' import { RiStickyNoteAddLine, RiThumbUpLine } from '@remixicon/react' import { useTranslation } from 'react-i18next' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Divider from '@/app/components/base/divider' export enum AppCategories { @@ -40,13 +40,13 @@ type CategoryItemProps = { } function CategoryItem({ category, active, onClick }: CategoryItemProps) { return
  • { onClick?.(category) }}> {category === AppCategories.RECOMMENDED &&
    } + className={cn('system-sm-medium text-components-menu-item-text group-hover:text-components-menu-item-text-hover group-[.active]:text-components-menu-item-text-active', active && 'system-sm-semibold')} />
  • } diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index a449ec8ef2..d74715187f 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -13,7 +13,7 @@ import AppIconPicker from '../../base/app-icon-picker' import type { AppIconSelection } from '../../base/app-icon-picker' import Button from '@/app/components/base/button' import Divider from '@/app/components/base/divider' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { basePath } from '@/utils/var' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 3564738dfd..0d30a2abac 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -25,7 +25,7 @@ import { useProviderContext } from '@/context/provider-context' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { getRedirection } from '@/utils/app-redirection' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { noop } from 'lodash-es' import { trackEvent } from '@/app/components/base/amplitude' diff --git a/web/app/components/app/create-from-dsl-modal/uploader.tsx b/web/app/components/app/create-from-dsl-modal/uploader.tsx index b6644da5a4..2745ca84c6 100644 --- a/web/app/components/app/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/app/create-from-dsl-modal/uploader.tsx @@ -8,7 +8,7 @@ import { import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import { formatFileSize } from '@/utils/format' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files' import { ToastContext } from '@/app/components/base/toast' import ActionButton from '@/app/components/base/action-button' diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index f98fb831ed..f25eb5373d 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { RiCloseLine } from '@remixicon/react' import AppIconPicker from '../../base/app-icon-picker' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' diff --git a/web/app/components/app/log-annotation/index.tsx b/web/app/components/app/log-annotation/index.tsx index c0b0854b29..e7c2be3eed 100644 --- a/web/app/components/app/log-annotation/index.tsx +++ b/web/app/components/app/log-annotation/index.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useRouter } from 'next/navigation' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Log from '@/app/components/app/log' import WorkflowLog from '@/app/components/app/workflow-log' import Annotation from '@/app/components/app/annotation' diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 0ff375d815..e479cbe881 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -39,7 +39,7 @@ import Tooltip from '@/app/components/base/tooltip' import CopyIcon from '@/app/components/base/copy-icon' import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { noop } from 'lodash-es' import PromptLogModal from '../../base/prompt-log-modal' import { WorkflowContextProvider } from '@/app/components/workflow/context' diff --git a/web/app/components/app/log/model-info.tsx b/web/app/components/app/log/model-info.tsx index 626ef093e9..b3c4f11be5 100644 --- a/web/app/components/app/log/model-info.tsx +++ b/web/app/components/app/log/model-info.tsx @@ -13,7 +13,7 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' const PARAM_MAP = { temperature: 'Temperature', diff --git a/web/app/components/app/log/var-panel.tsx b/web/app/components/app/log/var-panel.tsx index dd8c231a56..8915b3438a 100644 --- a/web/app/components/app/log/var-panel.tsx +++ b/web/app/components/app/log/var-panel.tsx @@ -9,7 +9,7 @@ import { } from '@remixicon/react' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import ImagePreview from '@/app/components/base/image-uploader/image-preview' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' type Props = { varList: { label: string; value: string }[] diff --git a/web/app/components/app/overview/apikey-info-panel/index.tsx b/web/app/components/app/overview/apikey-info-panel/index.tsx index b50b0077cb..47fe7af972 100644 --- a/web/app/components/app/overview/apikey-info-panel/index.tsx +++ b/web/app/components/app/overview/apikey-info-panel/index.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { RiCloseLine } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Button from '@/app/components/base/button' import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general' import { IS_CE_EDITION } from '@/config' diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index 6eba993e1d..d4be58b1b2 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -14,7 +14,7 @@ import type { SiteInfo } from '@/models/share' import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context' import ActionButton from '@/app/components/base/action-button' import { basePath } from '@/utils/var' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' type Props = { siteInfo?: SiteInfo diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 3b71b8f75c..d079631cf7 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -25,7 +25,7 @@ import { useModalContext } from '@/context/modal-context' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { useDocLink } from '@/context/i18n' export type ISettingsModalProps = { diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index a7e1cea429..742212a44d 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -6,7 +6,7 @@ import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' import { RiCloseLine } from '@remixicon/react' import AppIconPicker from '../../base/app-icon-picker' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import Checkbox from '@/app/components/base/checkbox' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index 92d86351e0..d284ecd46e 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -30,7 +30,7 @@ import type { SiteInfo } from '@/models/share' import { useChatContext } from '@/app/components/base/chat/chat/context' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' import NewAudioButton from '@/app/components/base/new-audio-button' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' const MAX_DEPTH = 3 diff --git a/web/app/components/app/text-generate/saved-items/index.tsx b/web/app/components/app/text-generate/saved-items/index.tsx index c22a4ca6c2..e6cf264cf2 100644 --- a/web/app/components/app/text-generate/saved-items/index.tsx +++ b/web/app/components/app/text-generate/saved-items/index.tsx @@ -8,7 +8,7 @@ import { import { useTranslation } from 'react-i18next' import copy from 'copy-to-clipboard' import NoData from './no-data' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import type { SavedMessage } from '@/models/debug' import { Markdown } from '@/app/components/base/markdown' import Toast from '@/app/components/base/toast' diff --git a/web/app/components/app/type-selector/index.tsx b/web/app/components/app/type-selector/index.tsx index 7be2351119..f213a89a94 100644 --- a/web/app/components/app/type-selector/index.tsx +++ b/web/app/components/app/type-selector/index.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next' import React, { useState } from 'react' import { RiArrowDownSLine, RiCloseCircleFill, RiExchange2Fill, RiFilter3Line } from '@remixicon/react' import Checkbox from '../../base/checkbox' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { PortalToFollowElem, PortalToFollowElemContent, diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx index 0e9b5dd67f..cef8a98f44 100644 --- a/web/app/components/app/workflow-log/list.tsx +++ b/web/app/components/app/workflow-log/list.tsx @@ -12,7 +12,7 @@ import Drawer from '@/app/components/base/drawer' import Indicator from '@/app/components/header/indicator' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import type { WorkflowRunTriggeredFrom } from '@/models/log' type ILogs = { diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index b8da0264e4..8140422c0f 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -5,7 +5,7 @@ import { useContext } from 'use-context-selector' import { useRouter } from 'next/navigation' import { useTranslation } from 'react-i18next' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import { type App, AppModeEnum } from '@/types/app' import Toast, { ToastContext } from '@/app/components/base/toast' import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' diff --git a/web/app/components/apps/new-app-card.tsx b/web/app/components/apps/new-app-card.tsx index 7a10bc8527..51e4bae8fe 100644 --- a/web/app/components/apps/new-app-card.tsx +++ b/web/app/components/apps/new-app-card.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next' import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal' import { useProviderContext } from '@/context/provider-context' import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files' -import cn from '@/utils/classnames' +import { cn } from '@/utils/classnames' import dynamic from 'next/dynamic' const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), { diff --git a/web/app/components/base/action-button/index.tsx b/web/app/components/base/action-button/index.tsx index f70bfb4448..eff6a43d22 100644 --- a/web/app/components/base/action-button/index.tsx +++ b/web/app/components/base/action-button/index.tsx @@ -1,7 +1,7 @@ import type { CSSProperties } from 'react' import React from 'react' import { type VariantProps, cva } from 'class-variance-authority' -import classNames from '@/utils/classnames' +import { cn } from '@/utils/classnames' enum ActionButtonState { Destructive = 'destructive', @@ -54,10 +54,8 @@ const ActionButton = ({ className, size, state = ActionButtonState.Default, styl return (