Compare commits
1 Commits
main
...
copilot/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
116e8bf6c9 |
@@ -54,6 +54,20 @@ describe('@langgenius/dify-ui/toast', () => {
|
||||
await expect.element(screen.getByText('Neutral toast')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should place an individual toast at its configured top position', async () => {
|
||||
const screen = await render(<ToastHost />)
|
||||
|
||||
toast.error('Positioned toast', {
|
||||
position: { top: 80 },
|
||||
})
|
||||
|
||||
const toastDialog = screen.getByRole('dialog', { name: 'Positioned toast' })
|
||||
await expect.element(toastDialog).toBeInTheDocument()
|
||||
await vi.waitFor(() => {
|
||||
expect(toastDialog.element().getBoundingClientRect().top).toBeCloseTo(80, 0)
|
||||
})
|
||||
})
|
||||
|
||||
it('should mark overflow toasts as limited when the stack exceeds the configured limit', async () => {
|
||||
const screen = await render(<ToastHost limit={1} />)
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@ import type * as React from 'react'
|
||||
import { Toast as BaseToast } from '@base-ui/react/toast'
|
||||
import { cn } from '../cn'
|
||||
|
||||
type ToastData = Record<string, never>
|
||||
type ToastData = {
|
||||
position?: {
|
||||
top: number
|
||||
}
|
||||
}
|
||||
type ToastToneStyle = {
|
||||
gradientClassName: string
|
||||
iconClassName: string
|
||||
@@ -53,6 +57,7 @@ type ToastAddOptions = Omit<
|
||||
ToastManagerAddOptions<ToastData>,
|
||||
'data' | 'positionerProps' | 'type'
|
||||
> & {
|
||||
position?: ToastData['position']
|
||||
type?: ToastType
|
||||
}
|
||||
|
||||
@@ -110,8 +115,11 @@ function getToastRenderType(type?: string): ToastRenderType | undefined {
|
||||
return type && isToastRenderType(type) ? type : undefined
|
||||
}
|
||||
|
||||
function addToast(options: ToastAddOptions) {
|
||||
return toastManager.add(options)
|
||||
function addToast({ position, ...options }: ToastAddOptions) {
|
||||
return toastManager.add({
|
||||
...options,
|
||||
data: { position },
|
||||
})
|
||||
}
|
||||
|
||||
const showToast: ToastCall = (title, options) =>
|
||||
@@ -168,6 +176,11 @@ function ToastCard({ toast: toastItem }: { toast: ToastObject<ToastData> }) {
|
||||
return (
|
||||
<BaseToast.Root
|
||||
toast={toastItem}
|
||||
style={
|
||||
toastItem.data?.position
|
||||
? { top: `calc(${toastItem.data.position.top}px - 1rem)` }
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
'pointer-events-auto absolute top-0 right-0 w-full origin-top cursor-default rounded-xl select-none focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
'[--toast-current-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:8px] [--toast-peek:5px] [--toast-scale:calc(1-(var(--toast-index)*0.0225))] [--toast-shrink:calc(1-var(--toast-scale))]',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
import ConfigurationView from '../configuration-view'
|
||||
|
||||
const mockIsAgentV2Enabled = vi.hoisted(() => vi.fn(() => false))
|
||||
let capturedModelParameterModalProps: Record<string, unknown> | null = null
|
||||
|
||||
vi.mock('@/features/agent-v2/feature-flag', () => ({
|
||||
isAgentV2Enabled: () => mockIsAgentV2Enabled(),
|
||||
@@ -37,7 +38,10 @@ vi.mock('@/app/components/app/configuration/config/agent-setting-button', () =>
|
||||
vi.mock(
|
||||
'@/app/components/header/account-setting/model-provider-page/model-parameter-modal',
|
||||
() => ({
|
||||
default: () => <div data-testid="model-parameter-modal" />,
|
||||
default: (props: Record<string, unknown>) => {
|
||||
capturedModelParameterModalProps = props
|
||||
return <div data-testid="model-parameter-modal" />
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -314,8 +318,25 @@ describe('ConfigurationView', () => {
|
||||
vi.clearAllMocks()
|
||||
mockIsAgentV2Enabled.mockReturnValue(false)
|
||||
pluginDependencyOnInstallComplete = undefined
|
||||
capturedModelParameterModalProps = null
|
||||
})
|
||||
|
||||
it.each([AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION])(
|
||||
'should position model parameter rule errors below the header for %s apps',
|
||||
(mode) => {
|
||||
const contextValue = createContextValue()
|
||||
contextValue.mode = mode
|
||||
|
||||
render(<ConfigurationView {...createViewModel({ contextValue })} />)
|
||||
|
||||
expect(capturedModelParameterModalProps).toEqual(
|
||||
expect.objectContaining({
|
||||
errorToastOptions: { position: { top: 60 } },
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
it('should render a loading state before configuration data is ready', () => {
|
||||
render(<ConfigurationView {...createViewModel({ showLoading: true })} />)
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import EditHistoryModal from '@/app/components/app/configuration/config-prompt/c
|
||||
import AgentSettingButton from '@/app/components/app/configuration/config/agent-setting-button'
|
||||
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
|
||||
import Debug from '@/app/components/app/configuration/debug'
|
||||
import { getDebugErrorToastOptions } from '@/app/components/app/configuration/debug/types'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { FeaturesProvider } from '@/app/components/base/features'
|
||||
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
|
||||
@@ -184,6 +185,7 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
|
||||
onCompletionParamsChange={onCompletionParamsChange}
|
||||
debugWithMultipleModel={debugWithMultipleModel}
|
||||
onDebugWithMultipleModelChange={onEnableMultipleModelDebug}
|
||||
errorToastOptions={getDebugErrorToastOptions(contextValue.mode)}
|
||||
/>
|
||||
<Divider type="vertical" className="mx-2 h-3.5" />
|
||||
</>
|
||||
|
||||
@@ -678,6 +678,7 @@ describe('Debug', () => {
|
||||
expect(notify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'appDebug.otherError.historyNoBeEmpty',
|
||||
position: { top: 60 },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -695,6 +696,7 @@ describe('Debug', () => {
|
||||
expect(notify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'appDebug.otherError.queryNoBeEmpty',
|
||||
position: { top: 60 },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -737,6 +739,7 @@ describe('Debug', () => {
|
||||
expect(notify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'appDebug.errorMessage.valueOfVarRequired:{"key":"Question"}',
|
||||
position: { top: 60 },
|
||||
})
|
||||
expect(mockState.mockSendCompletionMessage).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -846,10 +849,8 @@ describe('Debug', () => {
|
||||
fireEvent.click(screen.getByTestId('panel-send'))
|
||||
|
||||
await waitFor(() => expect(mockState.mockSendCompletionMessage).toHaveBeenCalledTimes(1))
|
||||
const [, requestData] = (mockState.mockSendCompletionMessage.mock.calls[0] ?? []) as [
|
||||
unknown,
|
||||
any,
|
||||
]
|
||||
const [, requestData, handlers] = (mockState.mockSendCompletionMessage.mock.calls[0] ??
|
||||
[]) as [unknown, any, Record<string, unknown>]
|
||||
expect(requestData).toMatchObject({
|
||||
inputs: { question: 'hello' },
|
||||
model_config: {
|
||||
@@ -863,6 +864,11 @@ describe('Debug', () => {
|
||||
expect(screen.getByTestId('text-generation'))!.toHaveTextContent('final answer')
|
||||
expect(screen.getByTestId('text-generation'))!.toHaveAttribute('data-message-id', 'msg-1')
|
||||
expect(screen.getByTestId('text-generation'))!.toHaveAttribute('data-tts', 'true')
|
||||
expect(handlers).toEqual(
|
||||
expect.objectContaining({
|
||||
errorToastOptions: { position: { top: 60 } },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should notify when sending again while a response is in progress', async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { screen } from '@testing-library/react'
|
||||
import { TransferMethod } from '@/app/components/base/chat/types'
|
||||
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { renderWithAccountProfile as render } from '@/test/console/account-profile'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART } from '../../types'
|
||||
import ChatItem from '../chat-item'
|
||||
|
||||
@@ -126,6 +127,7 @@ const createDefaultMocks = () => {
|
||||
inputs: { key: 'value' },
|
||||
collectionList: [],
|
||||
canTestAndRun: true,
|
||||
mode: AppModeEnum.CHAT,
|
||||
})
|
||||
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
@@ -338,7 +340,9 @@ describe('ChatItem', () => {
|
||||
query: 'Hello',
|
||||
inputs: { key: 'value' },
|
||||
}),
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
errorToastOptions: { position: { top: 60 } },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -361,6 +365,43 @@ describe('ChatItem', () => {
|
||||
expect(handleRestart).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should position error toasts below the header for agent-chat apps', () => {
|
||||
const handleSend = vi.fn()
|
||||
mockUseDebugConfigurationContext.mockReturnValue({
|
||||
modelConfig: {
|
||||
configs: { prompt_variables: [] },
|
||||
agentConfig: { tools: [] },
|
||||
},
|
||||
appId: 'app-123',
|
||||
inputs: { key: 'value' },
|
||||
collectionList: [],
|
||||
canTestAndRun: true,
|
||||
mode: AppModeEnum.AGENT_CHAT,
|
||||
})
|
||||
mockUseChat.mockReturnValue({
|
||||
chatList: [{ id: 'msg-1' }],
|
||||
isResponding: false,
|
||||
handleSend,
|
||||
suggestedQuestions: [],
|
||||
handleRestart: vi.fn(),
|
||||
})
|
||||
|
||||
renderComponent()
|
||||
|
||||
eventSubscriptionCallback?.({
|
||||
type: APP_CHAT_WITH_MULTIPLE_MODEL,
|
||||
payload: { message: 'Hello', files: [] },
|
||||
})
|
||||
|
||||
expect(handleSend).toHaveBeenCalledWith(
|
||||
'apps/app-123/chat-messages',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
errorToastOptions: { position: { top: 60 } },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should ignore unrelated events', () => {
|
||||
const handleSend = vi.fn()
|
||||
const handleRestart = vi.fn()
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ModelTypeEnum,
|
||||
PreferredProviderTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import ModelParameterTrigger from '../model-parameter-trigger'
|
||||
|
||||
const mockUseDebugConfigurationContext = vi.fn()
|
||||
@@ -38,6 +39,7 @@ let capturedModalProps: {
|
||||
debugWithMultipleModel: boolean
|
||||
onDebugWithMultipleModelChange: () => void
|
||||
renderTrigger: (props: RenderTriggerProps) => ReactNode
|
||||
errorToastOptions?: { position: { top: number } }
|
||||
} | null = null
|
||||
|
||||
vi.mock('@/context/debug-configuration', () => ({
|
||||
@@ -149,6 +151,7 @@ describe('ModelParameterTrigger', () => {
|
||||
|
||||
mockUseDebugConfigurationContext.mockReturnValue({
|
||||
isAdvancedMode: false,
|
||||
mode: AppModeEnum.CHAT,
|
||||
})
|
||||
|
||||
mockUseDebugWithMultipleModelContext.mockReturnValue({
|
||||
@@ -193,6 +196,7 @@ describe('ModelParameterTrigger', () => {
|
||||
expect(capturedModalProps?.modelId).toBe('claude-3')
|
||||
expect(capturedModalProps?.completionParams).toEqual({ max_tokens: 1000 })
|
||||
expect(capturedModalProps?.debugWithMultipleModel).toBe(true)
|
||||
expect(capturedModalProps?.errorToastOptions).toEqual({ position: { top: 60 } })
|
||||
})
|
||||
|
||||
it('should pass isAdvancedMode from context', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ModelAndParameter } from '../../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { TransferMethod } from '@/app/components/base/chat/types'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../../types'
|
||||
import TextGenerationItem from '../text-generation-item'
|
||||
|
||||
@@ -36,7 +37,7 @@ vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/text-generation/hooks', () => ({
|
||||
useTextGeneration: () => mockUseTextGeneration(),
|
||||
useTextGeneration: (...args: unknown[]) => mockUseTextGeneration(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
@@ -94,6 +95,7 @@ const createDefaultMocks = () => {
|
||||
completionPromptConfig: {},
|
||||
dataSets: [{ id: 'ds-1' }],
|
||||
datasetConfigs: { retrieval_model: 'single' },
|
||||
mode: AppModeEnum.COMPLETION,
|
||||
})
|
||||
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
@@ -165,6 +167,9 @@ describe('TextGenerationItem', () => {
|
||||
renderComponent()
|
||||
|
||||
expect(screen.getByTestId('text-generation')).toBeInTheDocument()
|
||||
expect(mockUseTextGeneration).toHaveBeenCalledWith({
|
||||
errorToastOptions: { position: { top: 60 } },
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass completion content to TextGeneration', () => {
|
||||
|
||||
@@ -21,7 +21,11 @@ import {
|
||||
} from '@/service/debug'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useConfigFromDebugContext, useFormattingChangedSubscription } from '../hooks'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART } from '../types'
|
||||
import {
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL,
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
|
||||
getDebugErrorToastOptions,
|
||||
} from '../types'
|
||||
|
||||
type ChatItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
@@ -34,7 +38,9 @@ const ChatItem: FC<ChatItemProps> = ({ modelAndParameter }) => {
|
||||
inputs,
|
||||
collectionList,
|
||||
canTestAndRun = false,
|
||||
mode,
|
||||
} = useDebugConfigurationContext()
|
||||
const errorToastOptions = getDebugErrorToastOptions(mode)
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const features = useFeatures((s) => s.features)
|
||||
const configTemplate = useConfigFromDebugContext()
|
||||
@@ -106,6 +112,7 @@ const ChatItem: FC<ChatItemProps> = ({ modelAndParameter }) => {
|
||||
fetchConversationMessages(appId, conversationId, getAbortController),
|
||||
onGetSuggestedQuestions: (responseItemId, getAbortController) =>
|
||||
fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
...(errorToastOptions && { errorToastOptions }),
|
||||
})
|
||||
},
|
||||
[
|
||||
@@ -113,6 +120,7 @@ const ChatItem: FC<ChatItemProps> = ({ modelAndParameter }) => {
|
||||
canTestAndRun,
|
||||
chatList,
|
||||
config,
|
||||
errorToastOptions,
|
||||
handleSend,
|
||||
inputs,
|
||||
modelAndParameter.model,
|
||||
|
||||
@@ -15,6 +15,7 @@ import ModelParameterModal from '@/app/components/header/account-setting/model-p
|
||||
import { useCredentialPanelState } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/use-credential-panel-state'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { getDebugErrorToastOptions } from '../types'
|
||||
import { useDebugWithMultipleModelContext } from './context'
|
||||
|
||||
type ModelParameterTriggerProps = {
|
||||
@@ -22,7 +23,7 @@ type ModelParameterTriggerProps = {
|
||||
}
|
||||
const ModelParameterTrigger: FC<ModelParameterTriggerProps> = ({ modelAndParameter }) => {
|
||||
const { t } = useTranslation()
|
||||
const { isAdvancedMode } = useDebugConfigurationContext()
|
||||
const { isAdvancedMode, mode } = useDebugConfigurationContext()
|
||||
const { multipleModelConfigs, onMultipleModelConfigsChange, onDebugWithMultipleModelChange } =
|
||||
useDebugWithMultipleModelContext()
|
||||
const { modelProviders } = useProviderContext()
|
||||
@@ -60,6 +61,7 @@ const ModelParameterTrigger: FC<ModelParameterTriggerProps> = ({ modelAndParamet
|
||||
setModel={handleSelectModel}
|
||||
debugWithMultipleModel
|
||||
onDebugWithMultipleModelChange={() => onDebugWithMultipleModelChange(modelAndParameter)}
|
||||
errorToastOptions={getDebugErrorToastOptions(mode)}
|
||||
renderTrigger={({ open, currentProvider, currentModel }) => {
|
||||
const status = deriveModelStatus(
|
||||
modelAndParameter.model,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL, getDebugErrorToastOptions } from '../types'
|
||||
|
||||
type TextGenerationItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
@@ -35,6 +35,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({ modelAndParameter })
|
||||
completionPromptConfig,
|
||||
dataSets,
|
||||
datasetConfigs,
|
||||
mode,
|
||||
} = useDebugConfigurationContext()
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const features = useFeatures((s) => s.features)
|
||||
@@ -76,7 +77,9 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({ modelAndParameter })
|
||||
},
|
||||
system_parameters: modelConfig.system_parameters,
|
||||
}
|
||||
const { completion, handleSend, isResponding, messageId } = useTextGeneration()
|
||||
const { completion, handleSend, isResponding, messageId } = useTextGeneration({
|
||||
errorToastOptions: getDebugErrorToastOptions(mode),
|
||||
})
|
||||
|
||||
const doSend: OnSend = (message, files) => {
|
||||
const currentProvider = textGenerationModelList.find(
|
||||
|
||||
@@ -673,6 +673,11 @@ describe('DebugWithSingleModel', () => {
|
||||
})
|
||||
|
||||
expect(mockSsePost.mock.calls[0]![0]).toBe('apps/test-app-id/chat-messages')
|
||||
expect(mockSsePost.mock.calls[0]![2]).toEqual(
|
||||
expect.objectContaining({
|
||||
errorToastOptions: { position: { top: 60 } },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should prevent send when checkCanSend returns false', async () => {
|
||||
@@ -694,6 +699,26 @@ describe('DebugWithSingleModel', () => {
|
||||
})
|
||||
expect(mockSsePost).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should position error toasts below the header for agent-chat apps', async () => {
|
||||
mockUseDebugConfigurationContext.mockReturnValue({
|
||||
...mockDebugConfigContext,
|
||||
mode: AppModeEnum.AGENT_CHAT,
|
||||
})
|
||||
|
||||
render(<DebugWithSingleModel ref={ref as RefObject<DebugWithSingleModelRefType>} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('send-button'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSsePost).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockSsePost.mock.calls[0]![2]).toEqual(
|
||||
expect.objectContaining({
|
||||
errorToastOptions: { position: { top: 60 } },
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// User Interactions
|
||||
|
||||
@@ -11,7 +11,6 @@ import { getLastAnswer, isValidGeneratedAnswer } from '@/app/components/base/cha
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { userProfileAtom } from '@/context/account-state'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
fetchConversationMessages,
|
||||
fetchSuggestedQuestions,
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
} from '@/service/debug'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useConfigFromDebugContext, useFormattingChangedSubscription } from '../hooks'
|
||||
import { getDebugErrorToastOptions } from '../types'
|
||||
|
||||
type DebugWithSingleModelProps = {
|
||||
checkCanSend?: () => boolean
|
||||
@@ -41,11 +41,12 @@ const DebugWithSingleModel = ({
|
||||
inputs,
|
||||
collectionList,
|
||||
completionParams,
|
||||
mode,
|
||||
// isShowVisionConfig,
|
||||
} = useDebugConfigurationContext()
|
||||
const errorToastOptions = getDebugErrorToastOptions(mode)
|
||||
const debugInputReadonly = !canTestAndRun
|
||||
const canManageAnnotation = !readonly && canTestAndRun
|
||||
const { textGenerationModelList } = useProviderContext()
|
||||
const features = useFeatures((s) => s.features)
|
||||
const configTemplate = useConfigFromDebugContext()
|
||||
const config = useMemo(() => {
|
||||
@@ -121,6 +122,7 @@ const DebugWithSingleModel = ({
|
||||
fetchConversationMessages(appId, conversationId, getAbortController),
|
||||
onGetSuggestedQuestions: (responseItemId, getAbortController) =>
|
||||
fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
...(errorToastOptions && { errorToastOptions }),
|
||||
})
|
||||
},
|
||||
[
|
||||
@@ -130,12 +132,12 @@ const DebugWithSingleModel = ({
|
||||
checkCanSend,
|
||||
completionParams,
|
||||
config,
|
||||
errorToastOptions,
|
||||
handleSend,
|
||||
inputs,
|
||||
modelConfig.mode,
|
||||
modelConfig.model_id,
|
||||
modelConfig.provider,
|
||||
textGenerationModelList,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -46,7 +46,11 @@ import FormattingChanged from '../base/warning-mask/formatting-changed'
|
||||
import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
|
||||
import DebugWithMultipleModel from './debug-with-multiple-model'
|
||||
import DebugWithSingleModel from './debug-with-single-model'
|
||||
import { APP_CHAT_WITH_MULTIPLE_MODEL, APP_CHAT_WITH_MULTIPLE_MODEL_RESTART } from './types'
|
||||
import {
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL,
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
|
||||
getDebugErrorToastOptions,
|
||||
} from './types'
|
||||
|
||||
type IDebug = {
|
||||
isAPIKeySet: boolean
|
||||
@@ -135,20 +139,25 @@ const Debug: FC<IDebug> = ({
|
||||
setIsShowFormattingChangeConfirm(false)
|
||||
setFormattingChanged(false)
|
||||
}
|
||||
const logError = useCallback((message: string) => {
|
||||
toast.error(message)
|
||||
}, [])
|
||||
const errorToastOptions = getDebugErrorToastOptions(mode)
|
||||
const logError = useCallback(
|
||||
(message: string) => {
|
||||
if (errorToastOptions) toast.error(message, errorToastOptions)
|
||||
else toast.error(message)
|
||||
},
|
||||
[errorToastOptions],
|
||||
)
|
||||
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||
|
||||
const checkCanSend = useCallback(() => {
|
||||
if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) {
|
||||
if (modelModeType === ModelModeType.completion) {
|
||||
if (!hasSetBlockStatus.history) {
|
||||
toast.error(t(($) => $['otherError.historyNoBeEmpty'], { ns: 'appDebug' }))
|
||||
logError(t(($) => $['otherError.historyNoBeEmpty'], { ns: 'appDebug' }))
|
||||
return false
|
||||
}
|
||||
if (!hasSetBlockStatus.query) {
|
||||
toast.error(t(($) => $['otherError.queryNoBeEmpty'], { ns: 'appDebug' }))
|
||||
logError(t(($) => $['otherError.queryNoBeEmpty'], { ns: 'appDebug' }))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -305,6 +314,7 @@ const Debug: FC<IDebug> = ({
|
||||
onError() {
|
||||
setRespondingFalse()
|
||||
},
|
||||
...(errorToastOptions && { errorToastOptions }),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type ModelAndParameter = {
|
||||
id: string
|
||||
model: string
|
||||
@@ -16,3 +18,15 @@ export type DebugWithSingleOrMultipleModelConfigs = {
|
||||
export const APP_CHAT_WITH_MULTIPLE_MODEL = 'APP_CHAT_WITH_MULTIPLE_MODEL'
|
||||
export const APP_CHAT_WITH_MULTIPLE_MODEL_RESTART = 'APP_CHAT_WITH_MULTIPLE_MODEL_RESTART'
|
||||
export const ORCHESTRATE_CHANGED = 'ORCHESTRATE_CHANGED'
|
||||
const DEBUG_ERROR_TOAST_OPTIONS = {
|
||||
position: { top: 60 },
|
||||
} as const
|
||||
|
||||
export const getDebugErrorToastOptions = (mode: AppModeEnum) => {
|
||||
if (
|
||||
mode === AppModeEnum.CHAT ||
|
||||
mode === AppModeEnum.AGENT_CHAT ||
|
||||
mode === AppModeEnum.COMPLETION
|
||||
)
|
||||
return DEBUG_ERROR_TOAST_OPTIONS
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ type SendCallback = {
|
||||
onUnhandledEvent?: IOtherOptions['onUnhandledEvent']
|
||||
onSendSettled?: (hasError?: boolean) => void
|
||||
isPublicAPI?: boolean
|
||||
errorToastOptions?: IOtherOptions['errorToastOptions']
|
||||
}
|
||||
|
||||
type UseChatOptions = {
|
||||
@@ -578,7 +579,13 @@ export const useChat = (
|
||||
async (
|
||||
messageId: string,
|
||||
workflowRunId: string,
|
||||
{ onGetSuggestedQuestions, onConversationComplete, onSendSettled, isPublicAPI }: SendCallback,
|
||||
{
|
||||
onGetSuggestedQuestions,
|
||||
onConversationComplete,
|
||||
onSendSettled,
|
||||
isPublicAPI,
|
||||
errorToastOptions,
|
||||
}: SendCallback,
|
||||
) => {
|
||||
const hasActiveSubscription =
|
||||
workflowEventsSubscriptionActiveRef.current &&
|
||||
@@ -600,6 +607,7 @@ export const useChat = (
|
||||
}
|
||||
const otherOptions: IOtherOptions = {
|
||||
isPublicAPI,
|
||||
...(errorToastOptions && { errorToastOptions }),
|
||||
getAbortController: (abortController) => {
|
||||
workflowEventsAbortControllerRef.current = abortController
|
||||
},
|
||||
@@ -1086,6 +1094,7 @@ export const useChat = (
|
||||
onUnhandledEvent,
|
||||
onSendSettled,
|
||||
isPublicAPI,
|
||||
errorToastOptions,
|
||||
}: SendCallback,
|
||||
) => {
|
||||
setSuggestedQuestions([])
|
||||
@@ -1186,6 +1195,7 @@ export const useChat = (
|
||||
|
||||
const otherOptions: IOtherOptions = {
|
||||
isPublicAPI,
|
||||
...(errorToastOptions && { errorToastOptions }),
|
||||
onUnhandledEvent,
|
||||
getAbortController: (abortController) => {
|
||||
workflowEventsAbortControllerRef.current = abortController
|
||||
|
||||
@@ -81,6 +81,17 @@ describe('useTextGeneration', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should forward configured error toast options to the stream request', async () => {
|
||||
const errorToastOptions = { position: { top: 60 } } as const
|
||||
const { result } = renderHook(() => useTextGeneration({ errorToastOptions }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSend('/console/api', { query: 'hello' })
|
||||
})
|
||||
|
||||
expect(getLatestStreamOptions().errorToastOptions).toEqual(errorToastOptions)
|
||||
})
|
||||
|
||||
it('should append chunks and update messageId when onData is triggered', async () => {
|
||||
const { result } = renderHook(() => useTextGeneration())
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ssePost } from '@/service/base'
|
||||
|
||||
export const useTextGeneration = () => {
|
||||
type UseTextGenerationOptions = {
|
||||
errorToastOptions?: Parameters<typeof toast.error>[1]
|
||||
}
|
||||
|
||||
export const useTextGeneration = ({ errorToastOptions }: UseTextGenerationOptions = {}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isResponding, setIsResponding] = useState(false)
|
||||
const [completion, setCompletion] = useState('')
|
||||
@@ -41,6 +45,7 @@ export const useTextGeneration = () => {
|
||||
onError() {
|
||||
setIsResponding(false)
|
||||
},
|
||||
...(errorToastOptions && { errorToastOptions }),
|
||||
},
|
||||
)
|
||||
return true
|
||||
|
||||
@@ -14,6 +14,7 @@ let parameterRules: Array<Record<string, unknown>> | undefined = [
|
||||
]
|
||||
let isRulesLoading = false
|
||||
let isRulesPending = false
|
||||
let modelParameterRulesArgs: unknown[] = []
|
||||
let currentProvider: Record<string, unknown> | undefined = {
|
||||
provider: 'openai',
|
||||
label: { en_US: 'OpenAI' },
|
||||
@@ -48,13 +49,16 @@ vi.mock('@/context/provider-context', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useModelParameterRules: () => ({
|
||||
data: {
|
||||
data: parameterRules,
|
||||
},
|
||||
isLoading: isRulesLoading,
|
||||
isPending: isRulesPending,
|
||||
}),
|
||||
useModelParameterRules: (...args: unknown[]) => {
|
||||
modelParameterRulesArgs = args
|
||||
return {
|
||||
data: {
|
||||
data: parameterRules,
|
||||
},
|
||||
isLoading: isRulesLoading,
|
||||
isPending: isRulesPending,
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks', () => ({
|
||||
@@ -197,6 +201,19 @@ describe('ModelParameterModal', () => {
|
||||
],
|
||||
},
|
||||
]
|
||||
modelParameterRulesArgs = []
|
||||
})
|
||||
|
||||
it('should pass error toast options to the parameter-rules query', () => {
|
||||
const errorToastOptions = { position: { top: 60 } } as const
|
||||
|
||||
render(<ModelParameterModal {...defaultProps} errorToastOptions={errorToastOptions} />)
|
||||
|
||||
expect(modelParameterRulesArgs).toEqual([
|
||||
defaultProps.provider,
|
||||
defaultProps.modelId,
|
||||
{ errorToastOptions },
|
||||
])
|
||||
})
|
||||
|
||||
it('should render trigger and open modal content when trigger is clicked', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { DefaultModel, FormValue, Model, ModelParameterRule } from '../decl
|
||||
import type { ParameterValue } from './parameter-item'
|
||||
import type { TriggerProps } from './types'
|
||||
import type { Node, NodeOutPutVar } from '@/app/components/workflow/types'
|
||||
import type { ModelParameterRulesQueryOptions } from '@/service/use-common'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useMemo, useState } from 'react'
|
||||
@@ -41,6 +42,7 @@ export type ModelParameterModalProps = {
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
modelList?: Model[]
|
||||
errorToastOptions?: ModelParameterRulesQueryOptions['errorToastOptions']
|
||||
}
|
||||
|
||||
const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
@@ -61,10 +63,13 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
|
||||
nodesOutputVars,
|
||||
availableNodes,
|
||||
modelList,
|
||||
errorToastOptions,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { data: parameterRulesData, isLoading } = useModelParameterRules(provider, modelId)
|
||||
const { data: parameterRulesData, isLoading } = useModelParameterRules(provider, modelId, {
|
||||
errorToastOptions,
|
||||
})
|
||||
const isRulesLoading = !!provider && !!modelId && isLoading
|
||||
const { currentProvider, currentModel, activeTextGenerationModelList } =
|
||||
useTextGenerationCurrentProviderAndModelAndModelList({ provider, model: modelId })
|
||||
|
||||
@@ -383,6 +383,27 @@ describe('ssePost and sseGet', () => {
|
||||
expect(toast.error).toHaveBeenCalledWith('TypeError: Network failed')
|
||||
})
|
||||
|
||||
it('should apply custom positioning to a stream error toast', async () => {
|
||||
const errorToastOptions = { position: { top: 80 } } as const
|
||||
vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new TypeError('Network failed'))
|
||||
|
||||
await ssePost(
|
||||
'/chat-messages',
|
||||
{
|
||||
body: {
|
||||
query: 'hello',
|
||||
},
|
||||
},
|
||||
{
|
||||
errorToastOptions,
|
||||
},
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith('TypeError: Network failed', errorToastOptions)
|
||||
})
|
||||
})
|
||||
|
||||
it('should report token refresh failures through onError', async () => {
|
||||
const onError = vi.fn()
|
||||
refreshAccessTokenOrReLoginMock.mockRejectedValueOnce(new Error('refresh failed'))
|
||||
|
||||
@@ -59,6 +59,11 @@ const isAbortError = (error: unknown) => {
|
||||
const shouldNotifyStreamError = (error: unknown) =>
|
||||
!isAbortError(error) && !String(error).includes('TypeError: Cannot assign to read only property')
|
||||
|
||||
const showErrorToast = (message: string, options?: Parameters<typeof toast.error>[1]) => {
|
||||
if (options) toast.error(message, options)
|
||||
else toast.error(message)
|
||||
}
|
||||
|
||||
export type IOnDataMoreInfo = {
|
||||
event?: string
|
||||
conversationId?: string
|
||||
@@ -120,6 +125,7 @@ export type IOtherOptions = {
|
||||
needAllResponseContent?: boolean
|
||||
deleteContentType?: boolean
|
||||
silent?: boolean
|
||||
errorToastOptions?: Parameters<typeof toast.error>[1]
|
||||
|
||||
/** If true, behaves like standard fetch: no URL prefix, returns raw Response */
|
||||
fetchCompat?: boolean
|
||||
@@ -556,6 +562,7 @@ export const ssePost = async (
|
||||
onDataSourceNodeCompleted,
|
||||
onDataSourceNodeError,
|
||||
onUnhandledEvent,
|
||||
errorToastOptions,
|
||||
} = otherOptions
|
||||
const abortController = new AbortController()
|
||||
|
||||
@@ -617,7 +624,7 @@ export const ssePost = async (
|
||||
}
|
||||
} else {
|
||||
res.json().then((data) => {
|
||||
toast.error(data.message || 'Server Error')
|
||||
showErrorToast(data.message || 'Server Error', errorToastOptions)
|
||||
})
|
||||
onError?.('Server Error')
|
||||
}
|
||||
@@ -629,7 +636,8 @@ export const ssePost = async (
|
||||
if (moreInfo.errorMessage) {
|
||||
onError?.(moreInfo.errorMessage, moreInfo.errorCode)
|
||||
// These errors can happen when a stream is intentionally stopped or its page is left.
|
||||
if (shouldNotifyStreamError(moreInfo.errorMessage)) toast.error(moreInfo.errorMessage)
|
||||
if (shouldNotifyStreamError(moreInfo.errorMessage))
|
||||
showErrorToast(moreInfo.errorMessage, errorToastOptions)
|
||||
return
|
||||
}
|
||||
onData?.(str, isFirstMessage, moreInfo)
|
||||
@@ -670,7 +678,7 @@ export const ssePost = async (
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMessage = String(e)
|
||||
if (shouldNotifyStreamError(e)) toast.error(errorMessage)
|
||||
if (shouldNotifyStreamError(e)) showErrorToast(errorMessage, errorToastOptions)
|
||||
onError?.(errorMessage)
|
||||
})
|
||||
}
|
||||
@@ -717,6 +725,7 @@ export const sseGet = async (
|
||||
onDataSourceNodeCompleted,
|
||||
onDataSourceNodeError,
|
||||
onUnhandledEvent,
|
||||
errorToastOptions,
|
||||
} = otherOptions
|
||||
const abortController = new AbortController()
|
||||
|
||||
@@ -772,7 +781,7 @@ export const sseGet = async (
|
||||
}
|
||||
} else {
|
||||
res.json().then((data) => {
|
||||
toast.error(data.message || 'Server Error')
|
||||
showErrorToast(data.message || 'Server Error', errorToastOptions)
|
||||
})
|
||||
onError?.('Server Error')
|
||||
}
|
||||
@@ -784,7 +793,8 @@ export const sseGet = async (
|
||||
if (moreInfo.errorMessage) {
|
||||
onError?.(moreInfo.errorMessage, moreInfo.errorCode)
|
||||
// These errors can happen when a stream is intentionally stopped or its page is left.
|
||||
if (shouldNotifyStreamError(moreInfo.errorMessage)) toast.error(moreInfo.errorMessage)
|
||||
if (shouldNotifyStreamError(moreInfo.errorMessage))
|
||||
showErrorToast(moreInfo.errorMessage, errorToastOptions)
|
||||
return
|
||||
}
|
||||
onData?.(str, isFirstMessage, moreInfo)
|
||||
@@ -825,7 +835,7 @@ export const sseGet = async (
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMessage = String(e)
|
||||
if (shouldNotifyStreamError(e)) toast.error(errorMessage)
|
||||
if (shouldNotifyStreamError(e)) showErrorToast(errorMessage, errorToastOptions)
|
||||
onError?.(errorMessage)
|
||||
})
|
||||
}
|
||||
@@ -975,7 +985,7 @@ export const request = async <T>(url: string, options = {}, otherOptions?: IOthe
|
||||
return Promise.reject(err)
|
||||
}
|
||||
if (code === 'init_validate_failed' && !silent) {
|
||||
toast.error(message, { timeout: 4000 })
|
||||
toast.error(message, { ...otherOptionsForBaseFetch.errorToastOptions, timeout: 4000 })
|
||||
return Promise.reject(err)
|
||||
}
|
||||
if (code === 'not_init_validated') {
|
||||
@@ -999,7 +1009,7 @@ export const request = async <T>(url: string, options = {}, otherOptions?: IOthe
|
||||
return Promise.reject(err)
|
||||
}
|
||||
if (!silent) {
|
||||
toast.error(message)
|
||||
showErrorToast(message, otherOptionsForBaseFetch.errorToastOptions)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
jumpTo(buildSigninUrlWithRedirect())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { IOnCompleted, IOnData, IOnError, IOnMessageReplace } from './base'
|
||||
import type { IOnCompleted, IOnData, IOnError, IOnMessageReplace, IOtherOptions } from './base'
|
||||
import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug'
|
||||
import type { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
import { get, post, ssePost } from './base'
|
||||
@@ -30,11 +30,13 @@ export const sendCompletionMessage = async (
|
||||
onCompleted,
|
||||
onError,
|
||||
onMessageReplace,
|
||||
errorToastOptions,
|
||||
}: {
|
||||
onData: IOnData
|
||||
onCompleted: IOnCompleted
|
||||
onError: IOnError
|
||||
onMessageReplace: IOnMessageReplace
|
||||
errorToastOptions?: IOtherOptions['errorToastOptions']
|
||||
},
|
||||
) => {
|
||||
return ssePost(
|
||||
@@ -45,7 +47,13 @@ export const sendCompletionMessage = async (
|
||||
response_mode: 'streaming',
|
||||
},
|
||||
},
|
||||
{ onData, onCompleted, onError, onMessageReplace },
|
||||
{
|
||||
onData,
|
||||
onCompleted,
|
||||
onError,
|
||||
onMessageReplace,
|
||||
...(errorToastOptions && { errorToastOptions }),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,31 @@ describe('base', () => {
|
||||
expect(toast.error).toHaveBeenCalledWith('Invalid DSL kind')
|
||||
})
|
||||
|
||||
it('should apply custom positioning to an error toast', async () => {
|
||||
const errorToastOptions = { position: { top: 60 } } as const
|
||||
const errorResponse = new Response(
|
||||
JSON.stringify({
|
||||
code: 'invalid_param',
|
||||
message: 'Invalid model',
|
||||
status: 400,
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue(errorResponse)
|
||||
|
||||
await expect(
|
||||
base('/model-parameter-rules', {}, { errorToastOptions }),
|
||||
).rejects.toBeInstanceOf(Response)
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith('Invalid model', errorToastOptions)
|
||||
})
|
||||
|
||||
it('should not display an empty error toast when message and error are absent', async () => {
|
||||
const errorResponse = new Response(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -77,7 +77,11 @@ const afterResponseErrorCode = (otherOptions: IOtherOptions): AfterResponseHook
|
||||
const shouldNotifyError = response.status !== 401 && errorData && !otherOptions.silent
|
||||
|
||||
const errorMessage = errorData?.message || errorData?.error
|
||||
if (shouldNotifyError && errorMessage) toast.error(errorMessage)
|
||||
if (shouldNotifyError && errorMessage) {
|
||||
if (otherOptions.errorToastOptions)
|
||||
toast.error(errorMessage, otherOptions.errorToastOptions)
|
||||
else toast.error(errorMessage)
|
||||
}
|
||||
|
||||
if (response.status === 403 && errorData?.code === 'already_setup')
|
||||
globalThis.location.href = `${globalThis.location.origin}/signin`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
import type { IOtherOptions } from './base'
|
||||
import type { FileTypesRes } from './datasets'
|
||||
import type {
|
||||
Model,
|
||||
@@ -248,14 +250,24 @@ export const useNotionBinding = (code?: string | null, enabled?: boolean) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const useModelParameterRules = (provider?: string, model?: string, enabled?: boolean) => {
|
||||
export type ModelParameterRulesQueryOptions = {
|
||||
enabled?: boolean
|
||||
errorToastOptions?: IOtherOptions['errorToastOptions']
|
||||
}
|
||||
|
||||
export const useModelParameterRules = (
|
||||
provider?: string,
|
||||
model?: string,
|
||||
options: ModelParameterRulesQueryOptions = {},
|
||||
) => {
|
||||
return useQuery<{ data: ModelParameterRule[] }>({
|
||||
queryKey: commonQueryKeys.modelParameterRules(provider, model),
|
||||
queryKey: [...commonQueryKeys.modelParameterRules(provider, model), options.errorToastOptions],
|
||||
queryFn: () =>
|
||||
get<{ data: ModelParameterRule[] }>(
|
||||
`/workspaces/current/model-providers/${provider}/models/parameter-rules`,
|
||||
{ params: { model }, silent: true },
|
||||
{ params: { model } },
|
||||
options.errorToastOptions ? { errorToastOptions: options.errorToastOptions } : undefined,
|
||||
),
|
||||
enabled: !!provider && !!model && (enabled ?? true),
|
||||
enabled: !!provider && !!model && (options.enabled ?? true),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user