From 20dea1faa23594922a90a4c6bfe565ffebbfc2e7 Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 25 Mar 2026 20:38:44 +0800 Subject: [PATCH] use base ui toast --- .../references/runtime-rules.md | 7 +- web/.storybook/preview.tsx | 7 +- .../apps/app-card-operations-flow.test.tsx | 34 +- .../datasets/create-dataset-flow.test.tsx | 2 +- .../dsl-export-import-flow.test.ts | 17 +- .../tools/tool-provider-detail-flow.test.tsx | 2 +- .../[appId]/overview/card-view.tsx | 9 +- .../[appId]/overview/tracing/panel.tsx | 7 +- .../tracing/provider-config-modal.tsx | 17 +- .../account-page/AvatarWithEdit.tsx | 80 ++- .../account-page/email-change-modal.tsx | 382 +++++++------ .../(commonLayout)/account-page/index.tsx | 216 ++++---- .../delete-account/components/feed-back.tsx | 4 +- web/app/components/app-sidebar/app-info.tsx | 2 +- .../__tests__/use-app-info-actions.spec.ts | 16 +- .../app-info/use-app-info-actions.ts | 33 +- .../app-sidebar/dataset-info/dropdown.tsx | 8 +- .../add-annotation-modal/index.spec.tsx | 2 +- .../annotation/add-annotation-modal/index.tsx | 7 +- .../csv-uploader.spec.tsx | 19 +- .../csv-uploader.tsx | 6 +- .../batch-add-annotation-modal/index.spec.tsx | 9 +- .../batch-add-annotation-modal/index.tsx | 11 +- .../edit-annotation-modal/index.spec.tsx | 37 +- .../edit-annotation-modal/index.tsx | 12 +- .../components/app/annotation/index.spec.tsx | 24 +- web/app/components/app/annotation/index.tsx | 22 +- .../access-control.spec.tsx | 6 +- .../app/app-access-control/index.tsx | 4 +- .../components/app/app-publisher/index.tsx | 6 +- .../app/app-publisher/version-info-modal.tsx | 12 +- .../config-prompt/advanced-prompt-input.tsx | 24 +- .../config-prompt/simple-prompt-input.tsx | 24 +- .../config-var/config-modal/index.tsx | 26 +- .../configuration/config-var/index.spec.tsx | 18 +- .../app/configuration/config-var/index.tsx | 9 +- .../config/agent/prompt-editor.tsx | 8 +- .../config/automatic/get-automatic-res.tsx | 25 +- .../configuration/config/automatic/result.tsx | 4 +- .../code-generator/get-code-generator-res.tsx | 18 +- .../params-config/config-content.spec.tsx | 18 +- .../params-config/config-content.tsx | 6 +- .../settings-modal/index.spec.tsx | 27 +- .../dataset-config/settings-modal/index.tsx | 11 +- .../debug-with-single-model/index.spec.tsx | 7 - .../app/configuration/debug/index.spec.tsx | 23 +- .../app/configuration/debug/index.tsx | 50 +- .../components/app/configuration/index.tsx | 19 +- .../tools/external-data-tool-modal.tsx | 248 ++++----- .../app/configuration/tools/index.tsx | 52 +- .../app/create-app-modal/index.spec.tsx | 19 +- .../components/app/create-app-modal/index.tsx | 2 +- .../app/create-from-dsl-modal/index.tsx | 192 ++++--- .../app/create-from-dsl-modal/uploader.tsx | 6 +- .../app/duplicate-modal/index.spec.tsx | 6 +- .../components/app/duplicate-modal/index.tsx | 4 +- web/app/components/app/log/list.tsx | 27 +- .../app/overview/settings/index.spec.tsx | 20 +- .../app/overview/settings/index.tsx | 9 +- .../app/switch-app-modal/index.spec.tsx | 31 +- .../components/app/switch-app-modal/index.tsx | 8 +- .../app/text-generate/item/index.tsx | 6 +- .../text-generate/saved-items/index.spec.tsx | 8 +- .../app/text-generate/saved-items/index.tsx | 4 +- web/app/components/apps/app-card.tsx | 479 ++++++++-------- .../agent-log-modal/__tests__/detail.spec.tsx | 35 +- .../agent-log-modal/__tests__/index.spec.tsx | 42 +- .../base/agent-log-modal/detail.tsx | 66 +-- .../base/agent-log-modal/index.stories.tsx | 7 +- .../base/audio-btn/__tests__/audio.spec.ts | 2 +- web/app/components/base/audio-btn/audio.ts | 20 +- .../base/audio-gallery/AudioPlayer.tsx | 88 +-- .../__tests__/AudioPlayer.spec.tsx | 16 +- .../base/block-input/__tests__/index.spec.tsx | 6 +- web/app/components/base/block-input/index.tsx | 47 +- .../__tests__/hooks.spec.tsx | 5 +- .../base/chat/chat-with-history/hooks.tsx | 166 +----- .../check-input-forms-hooks.spec.tsx | 4 +- .../base/chat/chat/__tests__/hooks.spec.tsx | 4 +- .../chat/chat/__tests__/question.spec.tsx | 4 +- .../chat/answer/__tests__/operation.spec.tsx | 2 +- .../base/chat/chat/answer/operation.tsx | 222 +------- .../chat-input-area/__tests__/index.spec.tsx | 4 +- .../base/chat/chat/chat-input-area/index.tsx | 164 +----- .../base/chat/chat/check-input-forms-hooks.ts | 18 +- .../base/chat/chat/hooks.hitl.spec.tsx | 6 +- web/app/components/base/chat/chat/hooks.ts | 6 +- .../components/base/chat/chat/question.tsx | 119 +--- .../embedded-chatbot/__tests__/hooks.spec.tsx | 5 +- .../base/chat/embedded-chatbot/hooks.tsx | 115 +--- .../inputs-form/__tests__/content.spec.tsx | 4 +- .../__tests__/annotation-ctrl-button.spec.tsx | 2 +- .../__tests__/config-param-modal.spec.tsx | 11 +- .../annotation-ctrl-button.tsx | 32 +- .../annotation-reply/config-param-modal.tsx | 47 +- .../moderation-setting-modal.spec.tsx | 4 +- .../moderation/moderation-setting-modal.tsx | 240 +++----- .../file-uploader/__tests__/hooks.spec.ts | 7 +- .../index.stories.tsx | 7 +- .../index.stories.tsx | 7 +- .../components/base/file-uploader/hooks.ts | 77 ++- .../__tests__/use-check-validated.spec.ts | 6 +- .../base/form/hooks/use-check-validated.ts | 17 +- .../image-uploader/__tests__/hooks.spec.ts | 4 +- .../__tests__/image-preview.spec.tsx | 2 +- .../components/base/image-uploader/hooks.ts | 112 ++-- .../base/image-uploader/image-preview.tsx | 102 +--- .../base/tag-input/__tests__/index.spec.tsx | 6 +- .../base/tag-input/__tests__/interop.spec.tsx | 32 +- web/app/components/base/tag-input/index.tsx | 112 ++-- .../tag-management/__tests__/index.spec.tsx | 28 +- .../tag-management/__tests__/panel.spec.tsx | 42 +- .../__tests__/selector.spec.tsx | 31 +- .../base/tag-management/index.stories.tsx | 7 +- .../components/base/tag-management/index.tsx | 43 +- .../components/base/tag-management/panel.tsx | 85 +-- .../base/tag-management/tag-item-editor.tsx | 45 +- .../text-generation/__tests__/hooks.spec.ts | 6 +- .../components/base/text-generation/hooks.ts | 57 +- .../base/toast/__tests__/index.spec.tsx | 349 ------------ web/app/components/base/toast/context.ts | 33 -- .../components/base/toast/index.stories.tsx | 105 ---- web/app/components/base/toast/index.tsx | 173 ------ .../components/base/toast/style.module.css | 44 -- .../custom-page/__tests__/index.spec.tsx | 22 +- .../__tests__/use-web-app-brand.spec.tsx | 22 +- .../hooks/use-web-app-brand.ts | 32 +- .../common/retrieval-param-config/index.tsx | 2 +- .../__tests__/uploader.spec.tsx | 24 +- .../hooks/use-dsl-import.ts | 67 +-- .../create-from-dsl-modal/uploader.tsx | 44 +- .../__tests__/index.spec.tsx | 28 +- .../empty-dataset-creation-modal/index.tsx | 26 +- .../hooks/__tests__/use-file-upload.spec.tsx | 45 +- .../file-uploader/hooks/use-file-upload.ts | 18 +- .../__tests__/use-document-creation.spec.ts | 2 +- .../step-two/hooks/use-document-creation.ts | 107 +--- .../datasets/create/step-two/index.tsx | 122 +---- .../create/website/firecrawl/index.tsx | 67 +-- .../create/website/jina-reader/index.tsx | 64 +-- .../create/website/watercrawl/index.tsx | 78 +-- .../components/__tests__/operations.spec.tsx | 39 +- .../documents/components/operations.tsx | 121 +---- .../__tests__/use-local-file-upload.spec.tsx | 57 +- .../documents/detail/__tests__/index.spec.tsx | 2 +- .../__tests__/csv-uploader.spec.tsx | 21 +- .../detail/batch-modal/csv-uploader.tsx | 55 +- .../detail/completed/__tests__/index.spec.tsx | 17 +- .../__tests__/use-child-segment-data.spec.ts | 4 +- .../__tests__/use-segment-list-data.spec.ts | 4 +- .../completed/hooks/use-child-segment-data.ts | 160 ++---- .../completed/hooks/use-segment-list-data.ts | 120 +--- .../detail/embedding/__tests__/index.spec.tsx | 33 +- .../documents/detail/embedding/index.tsx | 74 +-- .../datasets/documents/detail/index.tsx | 108 +--- .../__tests__/use-metadata-state.spec.ts | 23 +- .../metadata/hooks/use-metadata-state.ts | 29 +- .../datasets/documents/status-item/index.tsx | 70 +-- .../__tests__/index.spec.tsx | 6 +- .../external-api/external-api-modal/index.tsx | 85 +-- .../__tests__/modify-retrieval-modal.spec.tsx | 21 +- .../hit-testing/modify-retrieval-modal.tsx | 58 +- .../__tests__/modal.spec.tsx | 2 +- .../metadata/edit-metadata-batch/modal.tsx | 81 +-- .../use-batch-edit-document-metadata.spec.ts | 2 +- .../use-edit-dataset-metadata.spec.ts | 2 +- .../__tests__/use-metadata-document.spec.ts | 2 +- .../hooks/use-batch-edit-document-metadata.ts | 38 +- .../hooks/use-edit-dataset-metadata.ts | 38 +- .../metadata/hooks/use-metadata-document.ts | 41 +- .../dataset-metadata-drawer.spec.tsx | 2 +- .../dataset-metadata-drawer.tsx | 122 +---- .../rename-modal/__tests__/index.spec.tsx | 68 +-- .../datasets/rename-modal/index.tsx | 75 +-- .../explore/create-app-modal/index.tsx | 4 +- .../__tests__/compliance.spec.tsx | 18 +- .../header/account-dropdown/compliance.tsx | 97 +--- .../__tests__/index.spec.tsx | 5 +- .../workplace-selector/index.tsx | 99 ++-- .../__tests__/modal.spec.tsx | 35 +- .../api-based-extension-page/modal.tsx | 79 +-- .../language-page/__tests__/index.spec.tsx | 7 +- .../account-setting/language-page/index.tsx | 37 +- .../__tests__/dialog.spec.tsx | 5 +- .../__tests__/index.spec.tsx | 5 +- .../edit-workspace-modal/index.tsx | 55 +- .../operation/__tests__/index.spec.tsx | 5 +- .../members-page/operation/index.tsx | 58 +- .../__tests__/index.spec.tsx | 5 +- .../transfer-ownership-modal/index.tsx | 146 +---- .../hooks/__tests__/use-auth.spec.tsx | 11 +- .../model-auth/hooks/use-auth.ts | 98 +--- .../__tests__/credential-panel.spec.tsx | 2 +- .../model-load-balancing-modal.spec.tsx | 13 +- .../use-change-provider-priority.spec.ts | 2 +- .../use-activate-credential.spec.tsx | 18 +- .../use-activate-credential.ts | 35 +- .../model-load-balancing-modal.tsx | 269 +++------ .../use-change-provider-priority.ts | 44 +- .../plugin-page/SerpapiPlugin.tsx | 16 +- .../__tests__/SerpapiPlugin.spec.tsx | 27 +- .../plugin-page/__tests__/index.spec.tsx | 8 +- .../__tests__/authorized-in-node.spec.tsx | 4 - .../__tests__/plugin-auth-in-agent.spec.tsx | 4 - .../__tests__/api-key-modal.spec.tsx | 19 +- .../__tests__/authorize-components.spec.tsx | 16 +- .../__tests__/oauth-client-settings.spec.tsx | 19 +- .../plugin-auth/authorize/api-key-modal.tsx | 109 ++-- .../authorize/oauth-client-settings.tsx | 107 ++-- .../authorized/__tests__/index.spec.tsx | 18 +- .../plugins/plugin-auth/authorized/index.tsx | 132 ++--- .../__tests__/use-plugin-auth-action.spec.ts | 16 +- .../hooks/use-plugin-auth-action.ts | 28 +- .../datasource-action-list.tsx | 5 - .../edit/__tests__/apikey-edit-modal.spec.tsx | 29 +- .../edit/__tests__/manual-edit-modal.spec.tsx | 29 +- .../edit/__tests__/oauth-edit-modal.spec.tsx | 29 +- .../__tests__/use-reference-setting.spec.ts | 14 +- .../plugin-page/use-reference-setting.ts | 7 +- .../update-plugin/__tests__/index.spec.tsx | 25 +- .../update-plugin/from-market-place.tsx | 9 +- .../components/__tests__/conversion.spec.tsx | 34 +- .../__tests__/update-dsl-modal.spec.tsx | 17 +- .../rag-pipeline/components/conversion.tsx | 40 +- .../editor/form/__tests__/index.spec.tsx | 8 +- .../panel/input-field/editor/form/index.tsx | 35 +- .../field-list/__tests__/hooks.spec.ts | 2 +- .../field-list/__tests__/index.spec.tsx | 8 +- .../panel/input-field/field-list/hooks.ts | 41 +- .../__tests__/index.spec.tsx | 2 +- .../document-processing/options.tsx | 22 +- .../__tests__/index.spec.tsx | 17 +- .../publisher/__tests__/index.spec.tsx | 18 +- .../publisher/__tests__/popup.spec.tsx | 17 +- .../rag-pipeline-header/publisher/popup.tsx | 215 ++------ .../hooks/__tests__/index.spec.ts | 18 +- .../hooks/__tests__/use-DSL.spec.ts | 15 +- .../__tests__/use-update-dsl-modal.spec.ts | 21 +- .../components/rag-pipeline/hooks/use-DSL.ts | 25 +- .../hooks/use-update-dsl-modal.ts | 79 +-- .../use-text-generation-app-state.spec.ts | 2 +- .../hooks/use-text-generation-app-state.ts | 45 +- .../share/text-generation/index.tsx | 123 +---- .../result/__tests__/index.spec.tsx | 2 +- .../share/text-generation/result/index.tsx | 106 ++-- .../__tests__/index.spec.tsx | 14 +- .../edit-custom-collection-modal/index.tsx | 75 +-- .../tools/mcp/hooks/use-mcp-modal-form.ts | 22 +- .../__tests__/config-credentials.spec.tsx | 2 +- .../setting/build-in/config-credentials.tsx | 29 +- .../__tests__/features-trigger.spec.tsx | 21 +- .../workflow-header/features-trigger.tsx | 118 ++-- .../hooks/__tests__/use-DSL.spec.ts | 17 +- .../components/workflow-app/hooks/use-DSL.ts | 27 +- web/app/components/workflow/index.tsx | 1 + .../nodes/_base/hooks/use-one-step-run.ts | 2 +- .../workflow/nodes/trigger-webhook/panel.tsx | 2 +- .../plugins/link-editor-plugin/hooks.ts | 121 ++--- .../hooks/use-chat-message-sender.spec.ts | 16 +- .../hooks/file-tree/dnd/use-file-drop.ts | 2 +- .../operations/use-modify-operations.ts | 2 +- .../file-tree/operations/use-node-move.ts | 2 +- .../file-tree/operations/use-node-reorder.ts | 2 +- .../__tests__/value-content-sections.spec.tsx | 31 +- .../education-apply/education-apply-page.tsx | 8 +- web/app/init/InitPasswordPopup.tsx | 8 +- web/app/layout.tsx | 13 +- web/app/signin/invite-settings/page.tsx | 4 +- web/app/signin/one-more-step.tsx | 4 +- web/docs/overlay-migration.md | 17 +- web/eslint-suppressions.json | 511 +++--------------- web/eslint.constants.mjs | 9 - web/hooks/use-import-dsl.ts | 34 +- web/service/base.ts | 18 +- 274 files changed, 3597 insertions(+), 8129 deletions(-) delete mode 100644 web/app/components/base/toast/__tests__/index.spec.tsx delete mode 100644 web/app/components/base/toast/context.ts delete mode 100644 web/app/components/base/toast/index.stories.tsx delete mode 100644 web/app/components/base/toast/index.tsx delete mode 100644 web/app/components/base/toast/style.module.css diff --git a/.agents/skills/frontend-query-mutation/references/runtime-rules.md b/.agents/skills/frontend-query-mutation/references/runtime-rules.md index 02e8b9c2b6..73d6fbdded 100644 --- a/.agents/skills/frontend-query-mutation/references/runtime-rules.md +++ b/.agents/skills/frontend-query-mutation/references/runtime-rules.md @@ -64,7 +64,7 @@ export const useUpdateAccessMode = () => { // Component only adds UI behavior. updateAccessMode({ appId, mode }, { - onSuccess: () => Toast.notify({ type: 'success', message: '...' }), + onSuccess: () => toast.success('...'), }) // Avoid putting invalidation knowledge in the component. @@ -114,10 +114,7 @@ try { router.push(`/orders/${order.id}`) } catch (error) { - Toast.notify({ - type: 'error', - message: error instanceof Error ? error.message : 'Unknown error', - }) + toast.error(error instanceof Error ? error.message : 'Unknown error') } ``` diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index 072244c33f..a9144e7128 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -2,7 +2,7 @@ import type { Preview } from '@storybook/react' import type { Resource } from 'i18next' import { withThemeByDataAttribute } from '@storybook/addon-themes' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { ToastProvider } from '../app/components/base/toast' +import { ToastHost } from '../app/components/base/ui/toast' import { I18nClientProvider as I18N } from '../app/components/provider/i18n' import commonEnUS from '../i18n/en-US/common.json' @@ -39,9 +39,10 @@ export const decorators = [ return ( - + <> + - + ) diff --git a/web/__tests__/apps/app-card-operations-flow.test.tsx b/web/__tests__/apps/app-card-operations-flow.test.tsx index 5fd7e01561..be9ed9d079 100644 --- a/web/__tests__/apps/app-card-operations-flow.test.tsx +++ b/web/__tests__/apps/app-card-operations-flow.test.tsx @@ -25,6 +25,19 @@ let mockSystemFeatures = { const mockRouterPush = vi.fn() const mockNotify = vi.fn() +const mockToast = { + success: (message: string, options?: Record) => mockNotify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => mockNotify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => mockNotify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => mockNotify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +} + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) const mockOnPlanInfoChanged = vi.fn() const mockDeleteAppMutation = vi.fn().mockResolvedValue(undefined) let mockDeleteMutationPending = false @@ -94,27 +107,6 @@ vi.mock('@/context/provider-context', () => ({ }), })) -// Mock the ToastContext used via useContext from use-context-selector -vi.mock('use-context-selector', async () => { - const actual = await vi.importActual('use-context-selector') - return { - ...actual, - useContext: () => ({ notify: mockNotify }), - } -}) - -vi.mock('@/app/components/base/tag-management/store', () => ({ - useStore: (selector: (state: Record) => unknown) => { - const state = { - tagList: [], - showTagManagementModal: false, - setTagList: vi.fn(), - setShowTagManagementModal: vi.fn(), - } - return selector(state) - }, -})) - vi.mock('@/service/tag', () => ({ fetchTagList: vi.fn().mockResolvedValue([]), })) diff --git a/web/__tests__/datasets/create-dataset-flow.test.tsx b/web/__tests__/datasets/create-dataset-flow.test.tsx index e3a59edde6..3dba22e092 100644 --- a/web/__tests__/datasets/create-dataset-flow.test.tsx +++ b/web/__tests__/datasets/create-dataset-flow.test.tsx @@ -33,7 +33,7 @@ vi.mock('@/service/knowledge/use-dataset', () => ({ useInvalidDatasetList: () => vi.fn(), })) -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn() }, })) diff --git a/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts b/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts index dc5ab3fc86..cdf7aba4f6 100644 --- a/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts +++ b/web/__tests__/rag-pipeline/dsl-export-import-flow.test.ts @@ -10,6 +10,19 @@ import { describe, expect, it, vi } from 'vitest' const mockDoSyncWorkflowDraft = vi.fn().mockResolvedValue(undefined) const mockExportPipelineConfig = vi.fn().mockResolvedValue({ data: 'yaml-content' }) const mockNotify = vi.fn() +const mockToast = { + success: (message: string, options?: Record) => mockNotify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => mockNotify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => mockNotify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => mockNotify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +} + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) const mockEventEmitter = { emit: vi.fn() } const mockDownloadBlob = vi.fn() @@ -19,10 +32,6 @@ vi.mock('react-i18next', () => ({ }), })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ notify: mockNotify }), -})) - vi.mock('@/app/components/workflow/constants', () => ({ DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK', })) diff --git a/web/__tests__/tools/tool-provider-detail-flow.test.tsx b/web/__tests__/tools/tool-provider-detail-flow.test.tsx index 0101f83f22..bde620c1e2 100644 --- a/web/__tests__/tools/tool-provider-detail-flow.test.tsx +++ b/web/__tests__/tools/tool-provider-detail-flow.test.tsx @@ -153,7 +153,7 @@ vi.mock('@/app/components/base/confirm', () => ({ ), })) -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn() }, })) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index cd542cac9b..f0d21be910 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -8,12 +8,11 @@ import type { I18nKeysByPrefix } from '@/types/i18n' import * as React from 'react' import { useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import AppCard from '@/app/components/app/overview/app-card' import TriggerCard from '@/app/components/app/overview/trigger-card' import { useStore as useAppStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager' @@ -37,7 +36,6 @@ export type ICardViewProps = { const CardView: FC = ({ appId, isInPanel, className }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) @@ -106,10 +104,7 @@ const CardView: FC = ({ appId, isInPanel, className }) => { } } - notify({ - type, - message: t(`actionMsg.${message}`, { ns: 'common' }) as string, - }) + toast(t(`actionMsg.${message}`, { ns: 'common' }) as string, { type }) } // Listen for collaborative app state updates from other clients 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 1a2ec30ff9..696ea74728 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing' import Loading from '@/app/components/base/loading' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import Indicator from '@/app/components/header/indicator' import { useAppContext } from '@/context/app-context' import { usePathname } from '@/next/navigation' @@ -43,10 +43,7 @@ const Panel: FC = () => { await updateTracingStatus({ appId, body: tracingStatus }) setTracingStatus(tracingStatus) if (!noToast) { - Toast.notify({ - type: 'success', - message: t('api.success', { ns: 'common' }), - }) + toast(t('api.success', { ns: 'common' }), { type: 'success' }) } } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx index 0aa4c25989..2abaf7e8ec 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx @@ -14,7 +14,7 @@ import { PortalToFollowElem, PortalToFollowElemContent, } from '@/app/components/base/portal-to-follow-elem' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps' import { docURL } from './config' import Field from './field' @@ -155,10 +155,7 @@ const ProviderConfigModal: FC = ({ appId, provider: type, }) - Toast.notify({ - type: 'success', - message: t('api.remove', { ns: 'common' }), - }) + toast(t('api.remove', { ns: 'common' }), { type: 'success' }) onRemoved() hideRemoveConfirm() }, [hideRemoveConfirm, appId, type, t, onRemoved]) @@ -264,10 +261,7 @@ const ProviderConfigModal: FC = ({ return const errorMessage = checkValid() if (errorMessage) { - Toast.notify({ - type: 'error', - message: errorMessage, - }) + toast(errorMessage, { type: 'error' }) return } const action = isEdit ? updateTracingConfig : addTracingConfig @@ -279,10 +273,7 @@ const ProviderConfigModal: FC = ({ tracing_config: config, }, }) - Toast.notify({ - type: 'success', - message: t('api.success', { ns: 'common' }), - }) + toast(t('api.success', { ns: 'common' }), { type: 'success' }) onSaved(config) if (isAdd) onChosen(type) diff --git a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx index 3fc677d8d8..25e529a221 100644 --- a/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx +++ b/web/app/account/(commonLayout)/account-page/AvatarWithEdit.tsx @@ -8,15 +8,14 @@ import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import ImageInput from '@/app/components/base/app-icon-picker/ImageInput' import getCroppedImg from '@/app/components/base/app-icon-picker/utils' import { Avatar } from '@/app/components/base/avatar' import Button from '@/app/components/base/button' import Divider from '@/app/components/base/divider' import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks' -import Modal from '@/app/components/base/modal' -import { ToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { toast } from '@/app/components/base/ui/toast' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' import { updateUserProfile } from '@/service/common' @@ -25,7 +24,6 @@ type AvatarWithEditProps = AvatarProps & { onSave?: () => void } const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [inputImageInfo, setInputImageInfo] = useState() const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false) @@ -48,24 +46,24 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { await updateUserProfile({ url: 'account/avatar', body: { avatar: uploadedFileId } }) setIsShowAvatarPicker(false) onSave?.() - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) } catch (e) { - notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) } - }, [notify, onSave, t]) + }, [onSave, t]) const handleDeleteAvatar = useCallback(async () => { try { await updateUserProfile({ url: 'account/avatar', body: { avatar: '' } }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) setIsShowDeleteConfirm(false) onSave?.() } catch (e) { - notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) } - }, [notify, onSave, t]) + }, [onSave, t]) const { handleLocalFileUpload } = useLocalFileUploader({ limit: 3, @@ -134,45 +132,39 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { - setIsShowAvatarPicker(false)} - > - - + !open && setIsShowAvatarPicker(false)}> + + + -
- +
+ - -
- + +
+
+
- setIsShowDeleteConfirm(false)} - > -
{t('avatar.deleteTitle', { ns: 'common' })}
-

{t('avatar.deleteDescription', { ns: 'common' })}

+ !open && setIsShowDeleteConfirm(false)}> + +
{t('avatar.deleteTitle', { ns: 'common' })}
+

{t('avatar.deleteDescription', { ns: 'common' })}

-
- +
+ - -
- + +
+
+
) } diff --git a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx index f0dfd4f12f..2e2d61f2f9 100644 --- a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx +++ b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx @@ -1,14 +1,12 @@ import type { ResponseError } from '@/service/fetch' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'es-toolkit/function' import * as React from 'react' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' -import Modal from '@/app/components/base/modal' -import { ToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { toast } from '@/app/components/base/ui/toast' import { useRouter } from '@/next/navigation' import { checkEmailExisted, @@ -34,7 +32,6 @@ enum STEP { const EmailChangeModal = ({ onClose, email, show }: Props) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const router = useRouter() const [step, setStep] = useState(STEP.start) const [code, setCode] = useState('') @@ -70,10 +67,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { setStepToken(res.data) } catch (error) { - notify({ - type: 'error', - message: `Error sending verification code: ${error ? (error as any).message : ''}`, - }) + toast.error(`Error sending verification code: ${error ? (error as any).message : ''}`) } } @@ -89,17 +83,11 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { callback?.(res.token) } else { - notify({ - type: 'error', - message: 'Verifying email failed', - }) + toast.error('Verifying email failed') } } catch (error) { - notify({ - type: 'error', - message: `Error verifying email: ${error ? (error as any).message : ''}`, - }) + toast.error(`Error verifying email: ${error ? (error as any).message : ''}`) } } @@ -154,10 +142,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { const sendCodeToNewEmail = async () => { if (!isValidEmail(mail)) { - notify({ - type: 'error', - message: 'Invalid email format', - }) + toast.error('Invalid email format') return } await sendEmail( @@ -187,10 +172,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { handleLogout() } catch (error) { - notify({ - type: 'error', - message: `Error changing email: ${error ? (error as any).message : ''}`, - }) + toast.error(`Error changing email: ${error ? (error as any).message : ''}`) } } @@ -199,187 +181,185 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => { } return ( - -
- -
- {step === STEP.start && ( - <> -
{t('account.changeEmail.title', { ns: 'common' })}
-
-
{t('account.changeEmail.authTip', { ns: 'common' })}
-
- }} - values={{ email }} + !open && onClose()}> + +
+ +
+ {step === STEP.start && ( + <> +
{t('account.changeEmail.title', { ns: 'common' })}
+
+
{t('account.changeEmail.authTip', { ns: 'common' })}
+
+ }} + values={{ email }} + /> +
+
+
+
+ + +
+ + )} + {step === STEP.verifyOrigin && ( + <> +
{t('account.changeEmail.verifyEmail', { ns: 'common' })}
+
+
+ }} + values={{ email }} + /> +
+
+
+
{t('account.changeEmail.codeLabel', { ns: 'common' })}
+ setCode(e.target.value)} + maxLength={6} />
-
-
-
- - -
- - )} - {step === STEP.verifyOrigin && ( - <> -
{t('account.changeEmail.verifyEmail', { ns: 'common' })}
-
-
- }} - values={{ email }} +
+ + +
+
+ {t('account.changeEmail.resendTip', { ns: 'common' })} + {time > 0 && ( + {t('account.changeEmail.resendCount', { ns: 'common', count: time })} + )} + {!time && ( + {t('account.changeEmail.resend', { ns: 'common' })} + )} +
+ + )} + {step === STEP.newEmail && ( + <> +
{t('account.changeEmail.newEmail', { ns: 'common' })}
+
+
{t('account.changeEmail.content3', { ns: 'common' })}
+
+
+
{t('account.changeEmail.emailLabel', { ns: 'common' })}
+ handleNewEmailValueChange(e.target.value)} + destructive={newEmailExited || unAvailableEmail} + /> + {newEmailExited && ( +
{t('account.changeEmail.existingEmail', { ns: 'common' })}
+ )} + {unAvailableEmail && ( +
{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}
+ )} +
+
+ + +
+ + )} + {step === STEP.verifyNew && ( + <> +
{t('account.changeEmail.verifyNew', { ns: 'common' })}
+
+
+ }} + values={{ email: mail }} + /> +
+
+
+
{t('account.changeEmail.codeLabel', { ns: 'common' })}
+ setCode(e.target.value)} + maxLength={6} />
-
-
-
{t('account.changeEmail.codeLabel', { ns: 'common' })}
- setCode(e.target.value)} - maxLength={6} - /> -
-
- - -
-
- {t('account.changeEmail.resendTip', { ns: 'common' })} - {time > 0 && ( - {t('account.changeEmail.resendCount', { ns: 'common', count: time })} - )} - {!time && ( - {t('account.changeEmail.resend', { ns: 'common' })} - )} -
- - )} - {step === STEP.newEmail && ( - <> -
{t('account.changeEmail.newEmail', { ns: 'common' })}
-
-
{t('account.changeEmail.content3', { ns: 'common' })}
-
-
-
{t('account.changeEmail.emailLabel', { ns: 'common' })}
- handleNewEmailValueChange(e.target.value)} - destructive={newEmailExited || unAvailableEmail} - /> - {newEmailExited && ( -
{t('account.changeEmail.existingEmail', { ns: 'common' })}
- )} - {unAvailableEmail && ( -
{t('account.changeEmail.unAvailableEmail', { ns: 'common' })}
- )} -
-
- - -
- - )} - {step === STEP.verifyNew && ( - <> -
{t('account.changeEmail.verifyNew', { ns: 'common' })}
-
-
- }} - values={{ email: mail }} - /> +
+ +
-
-
-
{t('account.changeEmail.codeLabel', { ns: 'common' })}
- setCode(e.target.value)} - maxLength={6} - /> -
-
- - -
-
- {t('account.changeEmail.resendTip', { ns: 'common' })} - {time > 0 && ( - {t('account.changeEmail.resendCount', { ns: 'common', count: time })} - )} - {!time && ( - {t('account.changeEmail.resend', { ns: 'common' })} - )} -
- - )} - +
+ {t('account.changeEmail.resendTip', { ns: 'common' })} + {time > 0 && ( + {t('account.changeEmail.resendCount', { ns: 'common', count: time })} + )} + {!time && ( + {t('account.changeEmail.resend', { ns: 'common' })} + )} +
+ + )} + + ) } diff --git a/web/app/account/(commonLayout)/account-page/index.tsx b/web/app/account/(commonLayout)/account-page/index.tsx index 9a104619da..7b4a148530 100644 --- a/web/app/account/(commonLayout)/account-page/index.tsx +++ b/web/app/account/(commonLayout)/account-page/index.tsx @@ -7,13 +7,12 @@ import { import { useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' -import Modal from '@/app/components/base/modal' import PremiumBadge from '@/app/components/base/premium-badge' -import { ToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { toast } from '@/app/components/base/ui/toast' import Collapse from '@/app/components/header/account-setting/collapse' import { IS_CE_EDITION, validPassword } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -43,7 +42,6 @@ export default function AccountPage() { const userProfile = userProfileResp?.profile const mutateUserProfile = () => queryClient.invalidateQueries({ queryKey: commonQueryKeys.userProfile }) const { isEducationAccount } = useProviderContext() - const { notify } = useContext(ToastContext) const [editNameModalVisible, setEditNameModalVisible] = useState(false) const [editName, setEditName] = useState('') const [editing, setEditing] = useState(false) @@ -68,22 +66,19 @@ export default function AccountPage() { try { setEditing(true) await updateUserProfile({ url: 'account/name', body: { name: editName } }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) mutateUserProfile() setEditNameModalVisible(false) setEditing(false) } catch (e) { - notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) setEditing(false) } } const showErrorMessage = (message: string) => { - notify({ - type: 'error', - message, - }) + toast.error(message) } const valid = () => { if (!password.trim()) { @@ -119,14 +114,14 @@ export default function AccountPage() { repeat_new_password: confirmPassword, }, }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) mutateUserProfile() setEditPasswordModalVisible(false) resetPasswordForm() setEditing(false) } catch (e) { - notify({ type: 'error', message: (e as Error).message }) + toast.error((e as Error).message) setEditPasswordModalVisible(false) setEditing(false) } @@ -221,119 +216,112 @@ export default function AccountPage() {
{ editNameModalVisible && ( - setEditNameModalVisible(false)} - className="!w-[420px] !p-6" - > -
{t('account.editName', { ns: 'common' })}
-
{t('account.name', { ns: 'common' })}
- setEditName(e.target.value)} - /> -
- - -
-
+ !open && setEditNameModalVisible(false)}> + +
{t('account.editName', { ns: 'common' })}
+
{t('account.name', { ns: 'common' })}
+ setEditName(e.target.value)} + /> +
+ + +
+
+
) } { editPasswordModalVisible && ( - { - setEditPasswordModalVisible(false) - resetPasswordForm() - }} - className="!w-[420px] !p-6" - > -
{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}
- {userProfile.is_password_set && ( - <> -
{t('account.currentPassword', { ns: 'common' })}
-
- setCurrentPassword(e.target.value)} - /> + !open && (setEditPasswordModalVisible(false), resetPasswordForm())}> + +
{userProfile.is_password_set ? t('account.resetPassword', { ns: 'common' }) : t('account.setPassword', { ns: 'common' })}
+ {userProfile.is_password_set && ( + <> +
{t('account.currentPassword', { ns: 'common' })}
+
+ setCurrentPassword(e.target.value)} + /> -
- +
+ +
+ + )} +
+ {userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })} +
+
+ setPassword(e.target.value)} + /> +
+
- - )} -
- {userProfile.is_password_set ? t('account.newPassword', { ns: 'common' }) : t('account.password', { ns: 'common' })} -
-
- setPassword(e.target.value)} - /> -
+
+
{t('account.confirmPassword', { ns: 'common' })}
+
+ setConfirmPassword(e.target.value)} + /> +
+ +
+
+
+
-
-
{t('account.confirmPassword', { ns: 'common' })}
-
- setConfirmPassword(e.target.value)} - /> -
- -
-
-
- - -
- + +
) } { diff --git a/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx b/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx index af82d4bc62..0fd06599d2 100644 --- a/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx +++ b/web/app/account/(commonLayout)/delete-account/components/feed-back.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import CustomDialog from '@/app/components/base/dialog' import Textarea from '@/app/components/base/textarea' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useAppContext } from '@/context/app-context' import { useRouter } from '@/next/navigation' import { useLogout } from '@/service/use-common' @@ -28,7 +28,7 @@ export default function FeedBack(props: DeleteAccountProps) { await logout() // Tokens are now stored in cookies and cleared by backend router.push('/signin') - Toast.notify({ type: 'info', message: t('account.deleteSuccessTip', { ns: 'common' }) }) + toast.info(t('account.deleteSuccessTip', { ns: 'common' })) } catch (error) { console.error(error) } }, [router, t]) diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index 38b3cc3108..02cf281224 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -16,8 +16,8 @@ import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view' import { useStore as useAppStore } from '@/app/components/app/store' - import Button from '@/app/components/base/button' + import ContentDialog from '@/app/components/base/content-dialog' import { toast } from '@/app/components/base/ui/toast' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' diff --git a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts index deea28ce3e..1fe0b6ddb5 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts +++ b/web/app/components/app-sidebar/app-info/__tests__/use-app-info-actions.spec.ts @@ -27,10 +27,6 @@ vi.mock('@/next/navigation', () => ({ useRouter: () => ({ replace: mockReplace }), })) -vi.mock('use-context-selector', () => ({ - useContext: () => ({ notify: mockNotify }), -})) - vi.mock('@/context/provider-context', () => ({ useProviderContext: () => ({ onPlanInfoChanged: mockOnPlanInfoChanged }), })) @@ -42,8 +38,16 @@ vi.mock('@/app/components/app/store', () => ({ }), })) -vi.mock('@/app/components/base/toast/context', () => ({ - ToastContext: {}, +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: Object.assign(mockNotify, { + success: vi.fn((message, options) => mockNotify({ type: 'success', message, ...options })), + error: vi.fn((message, options) => mockNotify({ type: 'error', message, ...options })), + warning: vi.fn((message, options) => mockNotify({ type: 'warning', message, ...options })), + info: vi.fn((message, options) => mockNotify({ type: 'info', message, ...options })), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + }), })) vi.mock('@/service/use-apps', () => ({ diff --git a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts index 55ec13e506..8b559f7bba 100644 --- a/web/app/components/app-sidebar/app-info/use-app-info-actions.ts +++ b/web/app/components/app-sidebar/app-info/use-app-info-actions.ts @@ -3,9 +3,8 @@ import type { CreateAppModalProps } from '@/app/components/explore/create-app-mo import type { EnvironmentVariable } from '@/app/components/workflow/types' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useStore as useAppStore } from '@/app/components/app/store' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useProviderContext } from '@/context/provider-context' import { useRouter } from '@/next/navigation' @@ -24,7 +23,6 @@ type UseAppInfoActionsParams = { export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const { replace } = useRouter() const { onPlanInfoChanged } = useProviderContext() const appDetail = useAppStore(state => state.appDetail) @@ -72,13 +70,13 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { max_active_requests, }) closeModal() - notify({ type: 'success', message: t('editDone', { ns: 'app' }) }) + toast(t('editDone', { ns: 'app' }), { type: 'success' }) setAppDetail(app) } catch { - notify({ type: 'error', message: t('editFailed', { ns: 'app' }) }) + toast(t('editFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, closeModal, notify, setAppDetail, t]) + }, [appDetail, closeModal, setAppDetail, t]) const onCopy: DuplicateAppModalProps['onConfirm'] = useCallback(async ({ name, @@ -98,15 +96,15 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { mode: appDetail.mode, }) closeModal() - notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) }) + toast(t('newApp.appCreated', { ns: 'app' }), { type: 'success' }) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') onPlanInfoChanged() getRedirection(true, newApp, replace) } catch { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast(t('newApp.appCreateFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, closeModal, notify, onPlanInfoChanged, replace, t]) + }, [appDetail, closeModal, onPlanInfoChanged, replace, t]) const onExport = useCallback(async (include = false) => { if (!appDetail) @@ -117,9 +115,9 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { downloadBlob({ data: file, fileName: `${appDetail.name}.yml` }) } catch { - notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast(t('exportFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, notify, t]) + }, [appDetail, t]) const exportCheck = useCallback(async () => { if (!appDetail) @@ -145,29 +143,26 @@ export function useAppInfoActions({ onDetailExpand }: UseAppInfoActionsParams) { setSecretEnvList(list) } catch { - notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast(t('exportFailed', { ns: 'app' }), { type: 'error' }) } - }, [appDetail, closeModal, notify, onExport, t]) + }, [appDetail, closeModal, onExport, t]) const onConfirmDelete = useCallback(async () => { if (!appDetail) return try { await deleteApp(appDetail.id) - notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) }) + toast(t('appDeleted', { ns: 'app' }), { type: 'success' }) invalidateAppList() onPlanInfoChanged() setAppDetail() replace('/apps') } catch (e: unknown) { - notify({ - type: 'error', - message: `${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error && e.message ? `: ${e.message}` : ''}`, - }) + toast(`${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error && e.message ? `: ${e.message}` : ''}`, { type: 'error' }) } closeModal() - }, [appDetail, closeModal, invalidateAppList, notify, onPlanInfoChanged, replace, setAppDetail, t]) + }, [appDetail, closeModal, invalidateAppList, onPlanInfoChanged, replace, setAppDetail, t]) return { appDetail, diff --git a/web/app/components/app-sidebar/dataset-info/dropdown.tsx b/web/app/components/app-sidebar/dataset-info/dropdown.tsx index 528bac831f..1d1208e7d3 100644 --- a/web/app/components/app-sidebar/dataset-info/dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-info/dropdown.tsx @@ -3,6 +3,7 @@ import { RiMoreFill } from '@remixicon/react' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/components/base/ui/toast' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { useRouter } from '@/next/navigation' @@ -15,7 +16,6 @@ import { downloadBlob } from '@/utils/download' import ActionButton from '../../base/action-button' import Confirm from '../../base/confirm' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem' -import Toast from '../../base/toast' import RenameDatasetModal from '../../datasets/rename-modal' import Menu from './menu' @@ -69,7 +69,7 @@ const DropDown = ({ downloadBlob({ data: file, fileName: `${name}.pipeline` }) } catch { - Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast(t('exportFailed', { ns: 'app' }), { type: 'error' }) } }, [dataset, exportPipelineConfig, handleTrigger, t]) @@ -81,7 +81,7 @@ const DropDown = ({ } catch (e: any) { const res = await e.json() - Toast.notify({ type: 'error', message: res?.message || 'Unknown error' }) + toast(res?.message || 'Unknown error', { type: 'error' }) } finally { handleTrigger() @@ -91,7 +91,7 @@ const DropDown = ({ const onConfirmDelete = useCallback(async () => { try { await deleteDataset(dataset.id) - Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) }) + toast(t('datasetDeleted', { ns: 'dataset' }), { type: 'success' }) invalidDatasetList() replace('/datasets') } diff --git a/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx index bad3ceefdf..be6f17b88e 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/index.spec.tsx @@ -9,7 +9,7 @@ vi.mock('@/context/provider-context', () => ({ })) const mockToastNotify = vi.fn() -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn(args => mockToastNotify(args)), }, diff --git a/web/app/components/app/annotation/add-annotation-modal/index.tsx b/web/app/components/app/annotation/add-annotation-modal/index.tsx index ca808779c6..28e2c24382 100644 --- a/web/app/components/app/annotation/add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/add-annotation-modal/index.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Checkbox from '@/app/components/base/checkbox' import Drawer from '@/app/components/base/drawer-plus' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' import EditItem, { EditItemType } from './edit-item' @@ -47,10 +47,7 @@ const AddAnnotationModal: FC = ({ answer, } if (isValid(payload) !== true) { - Toast.notify({ - type: 'error', - message: isValid(payload) as string, - }) + toast.error(isValid(payload) as string) return } diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx index 55f5ee0564..603611ba9c 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.spec.tsx @@ -1,11 +1,23 @@ import type { Props } from './csv-uploader' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import { ToastContext } from '@/app/components/base/toast/context' import CSVUploader from './csv-uploader' describe('CSVUploader', () => { const notify = vi.fn() + const mockToast = { + success: (message: string, options?: Record) => notify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => notify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => notify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => notify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + } + + vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, + })) const updateFile = vi.fn() const getDropElements = () => { @@ -24,9 +36,8 @@ describe('CSVUploader', () => { ...props, } return render( - - - , + , + ) } 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 a969b3d491..0fbd3974aa 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,10 +4,9 @@ import { RiDeleteBinLine } from '@remixicon/react' import * as React from 'react' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import Button from '@/app/components/base/button' import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { cn } from '@/utils/classnames' export type Props = { @@ -20,7 +19,6 @@ const CSVUploader: FC = ({ updateFile, }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [dragging, setDragging] = useState(false) const dropRef = useRef(null) const dragRef = useRef(null) @@ -50,7 +48,7 @@ const CSVUploader: FC = ({ return const files = Array.from(e.dataTransfer.files) if (files.length > 1) { - notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) }) + toast.error(t('stepOne.uploader.validation.count', { ns: 'datasetCreation' })) return } updateFile(files[0]) diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx index 7fdb99fbab..e65632267c 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.spec.tsx @@ -2,17 +2,10 @@ import type { Mock } from 'vitest' import type { IBatchModalProps } from './index' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import Toast from '@/app/components/base/toast' import { useProviderContext } from '@/context/provider-context' import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' import BatchModal, { ProcessStatus } from './index' -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: vi.fn(), - }, -})) - vi.mock('@/service/annotation', () => ({ annotationBatchImport: vi.fn(), checkAnnotationBatchImportProgress: vi.fn(), @@ -49,7 +42,7 @@ vi.mock('@/app/components/billing/annotation-full', () => ({ default: () =>
, })) -const mockNotify = Toast.notify as Mock +const mockNotify = vi.fn() const useProviderContextMock = useProviderContext as Mock const annotationBatchImportMock = annotationBatchImport as Mock const checkAnnotationBatchImportProgressMock = checkAnnotationBatchImportProgress as Mock diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx index aaa8b6638e..47332e72d6 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' @@ -46,7 +46,6 @@ const BatchModal: FC = ({ }, [isShow]) const [importStatus, setImportStatus] = useState() - const notify = Toast.notify const checkProcess = async (jobID: string) => { try { const res = await checkAnnotationBatchImportProgress({ jobID, appId }) @@ -54,15 +53,15 @@ const BatchModal: FC = ({ if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING) setTimeout(() => checkProcess(res.job_id), 2500) if (res.job_status === ProcessStatus.ERROR) - notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}` }) + toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}`) if (res.job_status === ProcessStatus.COMPLETED) { - notify({ type: 'success', message: `${t('batchModal.completed', { ns: 'appAnnotation' })}` }) + toast.success(`${t('batchModal.completed', { ns: 'appAnnotation' })}`) onAdded() onCancel() } } catch (e: any) { - notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` }) + toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}`) } } @@ -78,7 +77,7 @@ const BatchModal: FC = ({ checkProcess(res.job_id) } catch (e: any) { - notify({ type: 'error', message: `${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}` }) + toast.error(`${t('batchModal.runError', { ns: 'appAnnotation' })}${'message' in e ? `: ${e.message}` : ''}`) } } 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 0bbd1ab67d..8f6dec42cf 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,7 +1,6 @@ -import type { IToastProps, ToastHandle } from '@/app/components/base/toast' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import EditAnnotationModal from './index' const { mockAddAnnotation, mockEditAnnotation } = vi.hoisted(() => ({ @@ -37,10 +36,8 @@ vi.mock('@/app/components/billing/annotation-full', () => ({ default: () =>
, })) -type ToastNotifyProps = Pick -type ToastWithNotify = typeof Toast & { notify: (props: ToastNotifyProps) => ToastHandle } -const toastWithNotify = Toast as unknown as ToastWithNotify -const toastNotifySpy = vi.spyOn(toastWithNotify, 'notify').mockReturnValue({ clear: vi.fn() }) +const toastSuccessSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success') +const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') describe('EditAnnotationModal', () => { const defaultProps = { @@ -55,7 +52,8 @@ describe('EditAnnotationModal', () => { } afterAll(() => { - toastNotifySpy.mockRestore() + toastSuccessSpy.mockRestore() + toastErrorSpy.mockRestore() }) beforeEach(() => { @@ -437,10 +435,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'API Error', - type: 'error', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('API Error') }) expect(mockOnAdded).not.toHaveBeenCalled() @@ -475,10 +470,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'common.api.actionFailed', - type: 'error', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('common.api.actionFailed') }) expect(mockOnAdded).not.toHaveBeenCalled() @@ -517,10 +509,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'API Error', - type: 'error', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('API Error') }) expect(mockOnEdited).not.toHaveBeenCalled() @@ -557,10 +546,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'common.api.actionFailed', - type: 'error', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('common.api.actionFailed') }) expect(mockOnEdited).not.toHaveBeenCalled() @@ -641,10 +627,7 @@ describe('EditAnnotationModal', () => { // Assert await waitFor(() => { - expect(toastNotifySpy).toHaveBeenCalledWith({ - message: 'common.api.actionSuccess', - type: 'success', - }) + expect(toastSuccessSpy).toHaveBeenCalledWith('common.api.actionSuccess') }) }) }) 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 c8da20a44a..855b827835 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next' import Confirm from '@/app/components/base/confirm' import Drawer from '@/app/components/base/drawer-plus' import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import AnnotationFull from '@/app/components/billing/annotation-full' import { useProviderContext } from '@/context/provider-context' import useTimestamp from '@/hooks/use-timestamp' @@ -72,18 +72,12 @@ const EditAnnotationModal: FC = ({ onAdded(res.id, res.account?.name ?? '', postQuery, postAnswer) } - Toast.notify({ - message: t('api.actionSuccess', { ns: 'common' }) as string, - type: 'success', - }) + toast.success(t('api.actionSuccess', { ns: 'common' }) as string) } catch (error) { const fallbackMessage = t('api.actionFailed', { ns: 'common' }) as string const message = error instanceof Error && error.message ? error.message : fallbackMessage - Toast.notify({ - message, - type: 'error', - }) + toast.error(message) // Re-throw to preserve edit mode behavior for UI components throw error } diff --git a/web/app/components/app/annotation/index.spec.tsx b/web/app/components/app/annotation/index.spec.tsx index d62b60d33d..5f5e9f74c0 100644 --- a/web/app/components/app/annotation/index.spec.tsx +++ b/web/app/components/app/annotation/index.spec.tsx @@ -3,7 +3,7 @@ import type { AnnotationItem } from './type' import type { App } from '@/types/app' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useProviderContext } from '@/context/provider-context' import { addAnnotation, @@ -17,10 +17,6 @@ import { AppModeEnum } from '@/types/app' import Annotation from './index' import { JobStatus } from './type' -vi.mock('@/app/components/base/toast', () => ({ - default: { notify: vi.fn() }, -})) - vi.mock('ahooks', () => ({ useDebounce: (value: any) => value, })) @@ -95,7 +91,23 @@ vi.mock('./view-annotation-modal', () => ({ vi.mock('@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal', () => ({ default: (props: any) => props.isShow ?
: null })) vi.mock('@/app/components/billing/annotation-full/modal', () => ({ default: (props: any) => props.show ?
: null })) -const mockNotify = Toast.notify as Mock +const mockNotify = vi.fn() +vi.spyOn(toast, 'success').mockImplementation((message, options) => { + mockNotify({ type: 'success', message, ...options }) + return 'toast-success-id' +}) +vi.spyOn(toast, 'error').mockImplementation((message, options) => { + mockNotify({ type: 'error', message, ...options }) + return 'toast-error-id' +}) +vi.spyOn(toast, 'warning').mockImplementation((message, options) => { + mockNotify({ type: 'warning', message, ...options }) + return 'toast-warning-id' +}) +vi.spyOn(toast, 'info').mockImplementation((message, options) => { + mockNotify({ type: 'info', message, ...options }) + return 'toast-info-id' +}) const addAnnotationMock = addAnnotation as Mock const delAnnotationMock = delAnnotation as Mock const delAnnotationsMock = delAnnotations as Mock diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index ee276603cc..0ea25744ff 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -15,6 +15,7 @@ import { MessageFast } from '@/app/components/base/icons/src/vender/solid/commun import Loading from '@/app/components/base/loading' import Pagination from '@/app/components/base/pagination' import Switch from '@/app/components/base/switch' +import { toast } from '@/app/components/base/ui/toast' import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' import { APP_PAGE_LIMIT } from '@/config' import { useProviderContext } from '@/context/provider-context' @@ -22,7 +23,6 @@ import { addAnnotation, delAnnotation, delAnnotations, fetchAnnotationConfig as import { AppModeEnum } from '@/types/app' import { sleep } from '@/utils' import { cn } from '@/utils/classnames' -import Toast from '../../base/toast' import EmptyElement from './empty-element' import Filter from './filter' import HeaderOpts from './header-opts' @@ -98,14 +98,14 @@ const Annotation: FC = (props) => { const handleAdd = async (payload: AnnotationItemBasic) => { await addAnnotation(appDetail.id, payload) - Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' }) + toast.success(t('api.actionSuccess', { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) } const handleRemove = async (id: string) => { await delAnnotation(appDetail.id, id) - Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' }) + toast.success(t('api.actionSuccess', { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) } @@ -113,13 +113,13 @@ const Annotation: FC = (props) => { const handleBatchDelete = async () => { try { await delAnnotations(appDetail.id, selectedIds) - Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' }) + toast.success(t('api.actionSuccess', { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) setSelectedIds([]) } catch (e: any) { - Toast.notify({ type: 'error', message: e.message || t('api.actionFailed', { ns: 'common' }) }) + toast.error(e.message || t('api.actionFailed', { ns: 'common' })) } } @@ -132,7 +132,7 @@ const Annotation: FC = (props) => { if (!currItem) return await editAnnotation(appDetail.id, currItem.id, { question, answer }) - Toast.notify({ message: t('api.actionSuccess', { ns: 'common' }), type: 'success' }) + toast.success(t('api.actionSuccess', { ns: 'common' })) fetchList() setControlUpdateList(Date.now()) } @@ -170,10 +170,7 @@ const Annotation: FC = (props) => { const { job_id: jobId }: any = await updateAnnotationStatus(appDetail.id, AnnotationEnableStatus.disable, annotationConfig?.embedding_model, annotationConfig?.score_threshold) await ensureJobCompleted(jobId, AnnotationEnableStatus.disable) await fetchAnnotationConfig() - Toast.notify({ - message: t('api.actionSuccess', { ns: 'common' }), - type: 'success', - }) + toast.success(t('api.actionSuccess', { ns: 'common' })) } }} > @@ -263,10 +260,7 @@ const Annotation: FC = (props) => { await updateAnnotationScore(appDetail.id, annotationId, score) await fetchAnnotationConfig() - Toast.notify({ - message: t('api.actionSuccess', { ns: 'common' }), - type: 'success', - }) + toast.success(t('api.actionSuccess', { ns: 'common' })) setIsShowEdit(false) }} annotationConfig={annotationConfig!} diff --git a/web/app/components/app/app-access-control/access-control.spec.tsx b/web/app/components/app/app-access-control/access-control.spec.tsx index 3950bdf7ee..289eccebb2 100644 --- a/web/app/components/app/app-access-control/access-control.spec.tsx +++ b/web/app/components/app/app-access-control/access-control.spec.tsx @@ -2,9 +2,9 @@ import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models import type { App } from '@/types/app' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { toast } from '@/app/components/base/ui/toast' import useAccessControlStore from '@/context/access-control-store' import { AccessMode, SubjectType } from '@/models/access-control' -import Toast from '../../base/toast' import AccessControlDialog from './access-control-dialog' import AccessControlItem from './access-control-item' import AddMemberOrGroupDialog from './add-member-or-group-pop' @@ -303,7 +303,7 @@ describe('AccessControl', () => { it('should initialize menu from app and call update on confirm', async () => { const onClose = vi.fn() const onConfirm = vi.fn() - const toastSpy = vi.spyOn(Toast, 'notify').mockReturnValue({}) + const toastSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success') useAccessControlStore.setState({ specificGroups: [baseGroup], specificMembers: [baseMember], @@ -336,7 +336,7 @@ describe('AccessControl', () => { { subjectId: baseMember.id, subjectType: SubjectType.ACCOUNT }, ], }) - expect(toastSpy).toHaveBeenCalled() + expect(toastSpy).toHaveBeenCalledWith('app.accessControlDialog.updateSuccess') expect(onConfirm).toHaveBeenCalled() }) }) diff --git a/web/app/components/app/app-access-control/index.tsx b/web/app/components/app/app-access-control/index.tsx index 70f77729ca..316433f41d 100644 --- a/web/app/components/app/app-access-control/index.tsx +++ b/web/app/components/app/app-access-control/index.tsx @@ -5,12 +5,12 @@ import { Description as DialogDescription, DialogTitle } from '@headlessui/react import { RiBuildingLine, RiGlobalLine, RiVerifiedBadgeLine } from '@remixicon/react' import { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/components/base/ui/toast' import { useGlobalPublicStore } from '@/context/global-public-context' import { AccessMode, SubjectType } from '@/models/access-control' import { useUpdateAccessMode } from '@/service/access-control' import useAccessControlStore from '../../../../context/access-control-store' import Button from '../../base/button' -import Toast from '../../base/toast' import AccessControlDialog from './access-control-dialog' import AccessControlItem from './access-control-item' import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-groups-or-members' @@ -61,7 +61,7 @@ export default function AccessControl(props: AccessControlProps) { submitData.subjects = subjects } await updateAccessMode(submitData) - Toast.notify({ type: 'success', message: t('accessControlDialog.updateSuccess', { ns: 'app' }) }) + toast.success(t('accessControlDialog.updateSuccess', { ns: 'app' })) onConfirm?.() }, [updateAccessMode, app, specificGroups, specificMembers, t, onConfirm, currentMenu]) return ( diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index dd988b89df..72d6490fa7 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -28,6 +28,7 @@ import { PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' +import { toast } from '@/app/components/base/ui/toast' import UpgradeBtn from '@/app/components/billing/upgrade-btn' import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' @@ -47,7 +48,6 @@ import { AppModeEnum } from '@/types/app' import { basePath } from '@/utils/var' import Divider from '../../base/divider' import Loading from '../../base/loading' -import Toast from '../../base/toast' import Tooltip from '../../base/tooltip' import ShortcutsName from '../../workflow/shortcuts-name' import { getKeyboardKeyCodeBySystem } from '../../workflow/utils' @@ -264,7 +264,7 @@ const AppPublisher = ({ throw new Error('No app found in Explore') }, { onError: (err) => { - Toast.notify({ type: 'error', message: `${err.message || err}` }) + toast.error(`${err.message || err}`) }, }) }, [appDetail?.id, openAsyncWindow]) @@ -290,7 +290,7 @@ const AppPublisher = ({ window.open(result.redirect_url, '_blank') } catch (error: any) { - Toast.notify({ type: 'error', message: error.message || t('common.publishToMarketplaceFailed', { ns: 'workflow' }) }) + toast.error(error.message || t('common.publishToMarketplaceFailed', { ns: 'workflow' })) } finally { setPublishingToMarketplace(false) diff --git a/web/app/components/app/app-publisher/version-info-modal.tsx b/web/app/components/app/app-publisher/version-info-modal.tsx index 45509d89ec..9713685d90 100644 --- a/web/app/components/app/app-publisher/version-info-modal.tsx +++ b/web/app/components/app/app-publisher/version-info-modal.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import Button from '../../base/button' import Input from '../../base/input' import Textarea from '../../base/textarea' @@ -35,10 +35,7 @@ const VersionInfoModal: FC = ({ const handlePublish = () => { if (title.length > TITLE_MAX_LENGTH) { setTitleError(true) - Toast.notify({ - type: 'error', - message: t('versionHistory.editField.titleLengthLimit', { ns: 'workflow', limit: TITLE_MAX_LENGTH }), - }) + toast.error(t('versionHistory.editField.titleLengthLimit', { ns: 'workflow', limit: TITLE_MAX_LENGTH })) return } else { @@ -48,10 +45,7 @@ const VersionInfoModal: FC = ({ if (releaseNotes.length > RELEASE_NOTES_MAX_LENGTH) { setReleaseNotesError(true) - Toast.notify({ - type: 'error', - message: t('versionHistory.editField.releaseNotesLengthLimit', { ns: 'workflow', limit: RELEASE_NOTES_MAX_LENGTH }), - }) + toast.error(t('versionHistory.editField.releaseNotesLengthLimit', { ns: 'workflow', limit: RELEASE_NOTES_MAX_LENGTH })) return } else { 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 9625204d81..482f61bb82 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 @@ -20,8 +20,12 @@ import { } from '@/app/components/base/icons/src/vender/line/files' import PromptEditor from '@/app/components/base/prompt-editor' import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' -import { useToastContext } from '@/app/components/base/toast/context' -import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import ConfigContext from '@/context/debug-configuration' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useModalContext } from '@/context/modal-context' @@ -74,7 +78,6 @@ const AdvancedPromptInput: FC = ({ showSelectDataSet, externalDataToolsConfig, } = useContext(ConfigContext) - const { notify } = useToastContext() const { setShowExternalDataToolModal } = useModalContext() const handleOpenExternalDataToolModal = () => { setShowExternalDataToolModal({ @@ -94,7 +97,7 @@ const AdvancedPromptInput: FC = ({ onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } @@ -180,13 +183,18 @@ const AdvancedPromptInput: FC = ({
{t('pageTitle.line1', { ns: 'appDebug' })}
- + + )} + /> +
{t('promptTip', { ns: 'appDebug' })}
- )} - /> +
+
)}
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 39a1699063..bc54e0f16d 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 @@ -17,8 +17,12 @@ import { useFeaturesStore } from '@/app/components/base/features/hooks' import PromptEditor from '@/app/components/base/prompt-editor' import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '@/app/components/base/prompt-editor/plugins/update-block' import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block' -import { useToastContext } from '@/app/components/base/toast/context' -import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import ConfigContext from '@/context/debug-configuration' import { useEventEmitterContextContext } from '@/context/event-emitter' import { useModalContext } from '@/context/modal-context' @@ -72,7 +76,6 @@ const Prompt: FC = ({ showSelectDataSet, externalDataToolsConfig, } = useContext(ConfigContext) - const { notify } = useToastContext() const { setShowExternalDataToolModal } = useModalContext() const handleOpenExternalDataToolModal = () => { setShowExternalDataToolModal({ @@ -92,7 +95,7 @@ const Prompt: FC = ({ onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } @@ -180,13 +183,18 @@ const Prompt: FC = ({
{mode !== AppModeEnum.COMPLETION ? t('chatSubTitle', { ns: 'appDebug' }) : t('completionSubTitle', { ns: 'appDebug' })}
{!readonly && ( - + + )} + /> +
{t('promptTip', { ns: 'appDebug' })}
- )} - /> +
+
)}
diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index 2bcdffa44d..c3d31b31d4 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -15,7 +15,7 @@ import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' import { SimpleSelect } from '@/app/components/base/select' import Textarea from '@/app/components/base/textarea' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting' @@ -98,10 +98,7 @@ const ConfigModal: FC = ({ const checkVariableName = useCallback((value: string, canBeEmpty?: boolean) => { const { isValid, errorMessageKey } = checkKeys([value], canBeEmpty) if (!isValid) { - Toast.notify({ - type: 'error', - message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) }), - }) + toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: t('variableConfig.varName', { ns: 'appDebug' }) })) return false } return true @@ -221,10 +218,7 @@ const ConfigModal: FC = ({ const value = e.target.value const { isValid, errorKey, errorMessageKey } = checkKeys([value], true) if (!isValid) { - Toast.notify({ - type: 'error', - message: t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey }), - }) + toast.error(t(`varKeyError.${errorMessageKey}`, { ns: 'appDebug', key: errorKey })) return } handlePayloadChange('variable')(e.target.value) @@ -266,7 +260,7 @@ const ConfigModal: FC = ({ return if (!tempPayload.label) { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.labelNameRequired', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.labelNameRequired', { ns: 'appDebug' })) return } if (isStringInput || type === InputVarType.number) { @@ -274,7 +268,7 @@ const ConfigModal: FC = ({ } else if (type === InputVarType.select) { if (options?.length === 0) { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.atLeastOneOption', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.atLeastOneOption', { ns: 'appDebug' })) return } const obj: Record = {} @@ -287,7 +281,7 @@ const ConfigModal: FC = ({ obj[o] = true }) if (hasRepeatedItem) { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.optionRepeat', { ns: 'appDebug' })) return } onConfirm(payloadToSave, moreInfo) @@ -297,12 +291,12 @@ const ConfigModal: FC = ({ )) { if (tempPayload.allowed_file_types?.length === 0) { const errorMessages = t('errorMsg.fieldRequired', { ns: 'workflow', field: t('variableConfig.file.supportFileTypes', { ns: 'appDebug' }) }) - Toast.notify({ type: 'error', message: errorMessages }) + toast.error(errorMessages) return } if (tempPayload.allowed_file_types?.includes(SupportUploadFileTypes.custom) && !tempPayload.allowed_file_extensions?.length) { const errorMessages = t('errorMsg.fieldRequired', { ns: 'workflow', field: t('variableConfig.file.custom.name', { ns: 'appDebug' }) }) - Toast.notify({ type: 'error', message: errorMessages }) + toast.error(errorMessages) return } onConfirm(payloadToSave, moreInfo) @@ -312,12 +306,12 @@ const ConfigModal: FC = ({ try { const schema = JSON.parse(normalizedJsonSchema) if (schema?.type !== 'object') { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.jsonSchemaMustBeObject', { ns: 'appDebug' })) return } } catch { - Toast.notify({ type: 'error', message: t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' }) }) + toast.error(t('variableConfig.errorMsg.jsonSchemaInvalid', { ns: 'appDebug' })) return } } diff --git a/web/app/components/app/configuration/config-var/index.spec.tsx b/web/app/components/app/configuration/config-var/index.spec.tsx index 096358c805..a48d3233f5 100644 --- a/web/app/components/app/configuration/config-var/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/index.spec.tsx @@ -5,13 +5,13 @@ import type { PromptVariable } from '@/models/debug' import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import * as React from 'react' import { vi } from 'vitest' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import DebugConfigurationContext from '@/context/debug-configuration' import { AppModeEnum } from '@/types/app' import ConfigVar, { ADD_EXTERNAL_DATA_TOOL } from './index' -const notifySpy = vi.spyOn(Toast, 'notify').mockImplementation(vi.fn()) +const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') const setShowExternalDataToolModal = vi.fn() @@ -112,7 +112,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should show empty state when no variables exist', () => { @@ -152,7 +152,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should add a text variable when selecting the string option', async () => { @@ -218,7 +218,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should save updates when editing a basic variable', async () => { @@ -268,7 +268,7 @@ describe('ConfigVar', () => { fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) - expect(Toast.notify).toHaveBeenCalled() + expect(toastErrorSpy).toHaveBeenCalled() expect(onPromptVariablesChange).not.toHaveBeenCalled() }) @@ -294,7 +294,7 @@ describe('ConfigVar', () => { fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) - expect(Toast.notify).toHaveBeenCalled() + expect(toastErrorSpy).toHaveBeenCalled() expect(onPromptVariablesChange).not.toHaveBeenCalled() }) }) @@ -306,7 +306,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should remove variable directly when context confirmation is not required', () => { @@ -359,7 +359,7 @@ describe('ConfigVar', () => { latestSortableProps = null subscriptionCallback = null variableIndex = 0 - notifySpy.mockClear() + toastErrorSpy.mockClear() }) it('should append external data tool variables from event emitter', () => { diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 4d9a4e480f..17f5e2efe5 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -12,8 +12,8 @@ import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' import { useContext } from 'use-context-selector' import Confirm from '@/app/components/base/confirm' -import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { InputVarType } from '@/app/components/workflow/types' import ConfigContext from '@/context/debug-configuration' import { useEventEmitterContextContext } from '@/context/event-emitter' @@ -108,10 +108,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar }) const duplicateError = getDuplicateError(newPromptVariables) if (duplicateError) { - Toast.notify({ - type: 'error', - message: t(duplicateError.errorMsgKey as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug', key: t(duplicateError.typeName as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug' }) }) as string, - }) + toast.error(t(duplicateError.errorMsgKey as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug', key: t(duplicateError.typeName as I18nKeysByPrefix<'appDebug', 'duplicateError.'>, { ns: 'appDebug' }) }) as string) return false } @@ -161,7 +158,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable && i !== index) { - Toast.notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } 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 f719d87261..e807c21518 100644 --- a/web/app/components/app/configuration/config/agent/prompt-editor.tsx +++ b/web/app/components/app/configuration/config/agent/prompt-editor.tsx @@ -12,7 +12,7 @@ import { CopyCheck, } from '@/app/components/base/icons/src/vender/line/files' import PromptEditor from '@/app/components/base/prompt-editor' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import ConfigContext from '@/context/debug-configuration' import { useModalContext } from '@/context/modal-context' import { cn } from '@/utils/classnames' @@ -32,8 +32,6 @@ const Editor: FC = ({ }) => { const { t } = useTranslation() - const { notify } = useToastContext() - const [isCopied, setIsCopied] = React.useState(false) const { modelConfig, @@ -59,14 +57,14 @@ const Editor: FC = ({ onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } for (let i = 0; i < externalDataToolsConfig.length; i++) { if (externalDataToolsConfig[i].variable === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: externalDataToolsConfig[i].variable }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: externalDataToolsConfig[i].variable })) return false } } diff --git a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx index c9cf4e926c..3f713a9317 100644 --- a/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx +++ b/web/app/components/app/configuration/config/automatic/get-automatic-res.tsx @@ -23,9 +23,9 @@ import Button from '@/app/components/base/button' import Confirm from '@/app/components/base/confirm' import { Generator } from '@/app/components/base/icons/src/vender/other' import Loading from '@/app/components/base/loading' - import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' + +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -161,13 +161,10 @@ const GetAutomaticRes: FC = ({ const isValid = () => { if (instruction.trim() === '') { - Toast.notify({ - type: 'error', - message: t('errorMsg.fieldRequired', { - ns: 'common', - field: t('generate.instruction', { ns: 'appDebug' }), - }), - }) + toast.error(t('errorMsg.fieldRequired', { + ns: 'common', + field: t('generate.instruction', { ns: 'appDebug' }), + })) return false } return true @@ -242,10 +239,7 @@ const GetAutomaticRes: FC = ({ } as GenRes if (error) { hasError = true - Toast.notify({ - type: 'error', - message: error, - }) + toast.error(error) } } else { @@ -260,10 +254,7 @@ const GetAutomaticRes: FC = ({ apiRes = res if (error) { hasError = true - Toast.notify({ - type: 'error', - message: error, - }) + toast.error(error) } } if (!hasError) diff --git a/web/app/components/app/configuration/config/automatic/result.tsx b/web/app/components/app/configuration/config/automatic/result.tsx index ef82007e51..776d774bd8 100644 --- a/web/app/components/app/configuration/config/automatic/result.tsx +++ b/web/app/components/app/configuration/config/automatic/result.tsx @@ -6,7 +6,7 @@ import copy from 'copy-to-clipboard' import * as React from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor' import PromptRes from './prompt-res' import PromptResInWorkflow from './prompt-res-in-workflow' @@ -54,7 +54,7 @@ const Result: FC = ({ className="px-2" onClick={() => { copy(current.modified) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.copySuccessfully', { ns: 'common' })) }} > diff --git a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx index b0833969ab..cfd58ac1db 100644 --- a/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx +++ b/web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx @@ -15,7 +15,7 @@ import Confirm from '@/app/components/base/confirm' import { Generator } from '@/app/components/base/icons/src/vender/other' import Loading from '@/app/components/base/loading' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' @@ -90,13 +90,10 @@ export const GetCodeGeneratorResModal: FC = ( const isValid = () => { if (instruction.trim() === '') { - Toast.notify({ - type: 'error', - message: t('errorMsg.fieldRequired', { - ns: 'common', - field: t('code.instruction', { ns: 'appDebug' }), - }), - }) + toast.error(t('errorMsg.fieldRequired', { + ns: 'common', + field: t('code.instruction', { ns: 'appDebug' }), + })) return false } return true @@ -149,10 +146,7 @@ export const GetCodeGeneratorResModal: FC = ( res.modified = (res as any).code if (error) { - Toast.notify({ - type: 'error', - message: error, - }) + toast.error(error) } else { addVersion(res) diff --git a/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx index 2cd8418c65..8a53e9a8b0 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx @@ -5,7 +5,7 @@ import type { DatasetConfigs } from '@/models/debug' import type { RetrievalConfig } from '@/types/app' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel, @@ -46,7 +46,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () const mockedUseModelListAndDefaultModelAndCurrentProviderAndModel = useModelListAndDefaultModelAndCurrentProviderAndModel as MockedFunction const mockedUseCurrentProviderAndModel = useCurrentProviderAndModel as MockedFunction -let toastNotifySpy: MockInstance +let toastErrorSpy: MockInstance const baseRetrievalConfig: RetrievalConfig = { search_method: RETRIEVE_METHOD.semantic, @@ -172,7 +172,7 @@ const createDatasetConfigs = (overrides: Partial = {}): DatasetC describe('ConfigContent', () => { beforeEach(() => { vi.clearAllMocks() - toastNotifySpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({})) + toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') mockedUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({ modelList: [], defaultModel: undefined, @@ -186,7 +186,7 @@ describe('ConfigContent', () => { }) afterEach(() => { - toastNotifySpy.mockRestore() + toastErrorSpy.mockRestore() }) // State management @@ -331,10 +331,7 @@ describe('ConfigContent', () => { await user.click(screen.getByText('common.modelProvider.rerankModel.key')) // Assert - expect(toastNotifySpy).toHaveBeenCalledWith({ - type: 'error', - message: 'workflow.errorMsg.rerankModelRequired', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired') expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ reranking_mode: RerankingModeEnum.RerankingModel, @@ -373,10 +370,7 @@ describe('ConfigContent', () => { await user.click(screen.getByRole('switch')) // Assert - expect(toastNotifySpy).toHaveBeenCalledWith({ - type: 'error', - message: 'workflow.errorMsg.rerankModelRequired', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired') expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ reranking_enable: true, 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 6dd03d217e..be0d1d9394 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 @@ -15,8 +15,8 @@ import Divider from '@/app/components/base/divider' import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold-item' import TopKItem from '@/app/components/base/param-item/top-k-item' import Switch from '@/app/components/base/switch' -import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' @@ -136,7 +136,7 @@ const ConfigContent: FC = ({ return if (mode === RerankingModeEnum.RerankingModel && !currentRerankModel) - Toast.notify({ type: 'error', message: t('errorMsg.rerankModelRequired', { ns: 'workflow' }) }) + toast.error(t('errorMsg.rerankModelRequired', { ns: 'workflow' })) onChange({ ...datasetConfigs, @@ -179,7 +179,7 @@ const ConfigContent: FC = ({ const handleManuallyToggleRerank = useCallback((enable: boolean) => { if (!currentRerankModel && enable) - Toast.notify({ type: 'error', message: t('errorMsg.rerankModelRequired', { ns: 'workflow' }) }) + toast.error(t('errorMsg.rerankModelRequired', { ns: 'workflow' })) onChange({ ...datasetConfigs, reranking_enable: enable, diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.spec.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.spec.tsx index 264e66fd96..1de9437745 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.spec.tsx @@ -3,7 +3,6 @@ import type { DataSet } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { ToastContext } from '@/app/components/base/toast/context' import { IndexingType } from '@/app/components/datasets/create/step-two' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' @@ -14,6 +13,19 @@ import { RETRIEVE_METHOD } from '@/types/app' import SettingsModal from './index' const mockNotify = vi.fn() +const mockToast = { + success: (message: string, options?: Record) => mockNotify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => mockNotify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => mockNotify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => mockNotify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +} + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) const mockOnCancel = vi.fn() const mockOnSave = vi.fn() const mockSetShowAccountSettingModal = vi.fn() @@ -183,13 +195,12 @@ const createDataset = (overrides: Partial = {}, retrievalOverrides: Par const renderWithProviders = (dataset: DataSet) => { return render( - - - , + , + ) } 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 4435e1b311..7ec51f4fa5 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 @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' import { IndexingType } from '@/app/components/datasets/create/step-two' import IndexMethod from '@/app/components/datasets/settings/index-method' @@ -51,7 +51,6 @@ const SettingsModal: FC = ({ const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank) const { t } = useTranslation() const docLink = useDocLink() - const { notify } = useToastContext() const ref = useRef(null) const isExternal = currentDataset.provider === 'external' const { setShowAccountSettingModal } = useModalContext() @@ -96,7 +95,7 @@ const SettingsModal: FC = ({ if (loading) return if (!localeCurrentDataset.name?.trim()) { - notify({ type: 'error', message: t('form.nameError', { ns: 'datasetSettings' }) }) + toast.error(t('form.nameError', { ns: 'datasetSettings' })) return } if ( @@ -106,7 +105,7 @@ const SettingsModal: FC = ({ indexMethod, }) ) { - notify({ type: 'error', message: t('datasetConfig.rerankModelRequired', { ns: 'appDebug' }) }) + toast.error(t('datasetConfig.rerankModelRequired', { ns: 'appDebug' })) return } try { @@ -146,7 +145,7 @@ const SettingsModal: FC = ({ }) } await updateDatasetSetting(requestParams) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) onSave({ ...localeCurrentDataset, indexing_technique: indexMethod, @@ -154,7 +153,7 @@ const SettingsModal: FC = ({ }) } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) } finally { setLoading(false) 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 a75516a43f..910a8fd2b5 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 @@ -386,13 +386,6 @@ vi.mock('@/context/event-emitter', () => ({ })), })) -// Mock toast context -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: vi.fn(() => ({ - notify: vi.fn(), - })), -})) - // Mock hooks/use-timestamp vi.mock('@/hooks/use-timestamp', () => ({ default: vi.fn(() => ({ diff --git a/web/app/components/app/configuration/debug/index.spec.tsx b/web/app/components/app/configuration/debug/index.spec.tsx index e94695f1ef..6b7d49bb56 100644 --- a/web/app/components/app/configuration/debug/index.spec.tsx +++ b/web/app/components/app/configuration/debug/index.spec.tsx @@ -1,7 +1,6 @@ import type { ComponentProps } from 'react' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' -import { ToastContext } from '@/app/components/base/toast/context' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import ConfigContext from '@/context/debug-configuration' import { AppModeEnum, ModelModeType, TransferMethod } from '@/types/app' @@ -377,6 +376,19 @@ const renderDebug = (options: { } = {}) => { const onSetting = vi.fn() const notify = vi.fn() + const mockToast = { + success: (message: string, options?: Record) => notify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => notify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => notify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => notify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + } + + vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, + })) const props: ComponentProps = { isAPIKeySet: true, onSetting, @@ -392,11 +404,10 @@ const renderDebug = (options: { } render( - - - - - , + + + , + ) return { onSetting, notify, props } diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index cd07885f0c..36cd4c3445 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -29,8 +29,12 @@ import Button from '@/app/components/base/button' import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows' import PromptLogModal from '@/app/components/base/prompt-log-modal' -import { ToastContext } from '@/app/components/base/toast/context' -import TooltipPlus from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config' @@ -139,22 +143,20 @@ const Debug: FC = ({ setIsShowFormattingChangeConfirm(false) setFormattingChanged(false) } - - const { notify } = useContext(ToastContext) const logError = useCallback((message: string) => { - notify({ type: 'error', message }) - }, [notify]) + toast.error(message) + }, []) const [completionFiles, setCompletionFiles] = useState([]) const checkCanSend = useCallback(() => { if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) { if (modelModeType === ModelModeType.completion) { if (!hasSetBlockStatus.history) { - notify({ type: 'error', message: t('otherError.historyNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.historyNoBeEmpty', { ns: 'appDebug' })) return false } if (!hasSetBlockStatus.query) { - notify({ type: 'error', message: t('otherError.queryNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.queryNoBeEmpty', { ns: 'appDebug' })) return false } } @@ -180,7 +182,7 @@ const Debug: FC = ({ } if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) { - notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) }) + toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' })) return false } return !hasEmptyInput @@ -194,7 +196,6 @@ const Debug: FC = ({ modelConfig.configs.prompt_variables, t, logError, - notify, modelModeType, ]) @@ -205,7 +206,7 @@ const Debug: FC = ({ const sendTextCompletion = async () => { if (isResponding) { - notify({ type: 'info', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) }) + toast.info(t('errorMessage.waitForResponse', { ns: 'appDebug' })) return false } @@ -420,27 +421,24 @@ const Debug: FC = ({ <> { !readonly && ( - - - - - - + + } /> + + {t('operation.refresh', { ns: 'common' })} + + ) } { varList.length > 0 && (
- - !readonly && setExpanded(!expanded)}> - - - + + !readonly && setExpanded(!expanded)}>} /> + + {t('panel.userInputField', { ns: 'workflow' })} + + {expanded &&
}
) diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 6045c7819e..a96d4487f1 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -27,7 +27,6 @@ import { produce } from 'immer' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useShallow } from 'zustand/react/shallow' import AppPublisher from '@/app/components/app/app-publisher/features-wrapper' import Config from '@/app/components/app/configuration/config' @@ -49,8 +48,7 @@ import { FeaturesProvider } from '@/app/components/base/features' import NewFeaturePanel from '@/app/components/base/features/new-feature-panel' import Loading from '@/app/components/base/loading' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' -import Toast from '@/app/components/base/toast' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { ModelFeatureEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { @@ -94,7 +92,6 @@ type PublishConfig = { const Configuration: FC = () => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const { isLoadingCurrentWorkspace, currentWorkspace } = useAppContext() const { appDetail, showAppConfigureFeaturesModal, setAppSidebarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({ @@ -493,11 +490,11 @@ const Configuration: FC = () => { isAdvancedMode, ) if (Object.keys(removedDetails).length) - Toast.notify({ type: 'warning', message: `${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}` }) + toast.warning(`${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}`) setCompletionParams(filtered) } catch { - Toast.notify({ type: 'error', message: t('error', { ns: 'common' }) }) + toast.error(t('error', { ns: 'common' })) setCompletionParams({}) } } @@ -769,23 +766,23 @@ const Configuration: FC = () => { const promptVariables = modelConfig.configs.prompt_variables if (promptEmpty) { - notify({ type: 'error', message: t('otherError.promptNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.promptNoBeEmpty', { ns: 'appDebug' })) return } if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) { if (modelModeType === ModelModeType.completion) { if (!hasSetBlockStatus.history) { - notify({ type: 'error', message: t('otherError.historyNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.historyNoBeEmpty', { ns: 'appDebug' })) return } if (!hasSetBlockStatus.query) { - notify({ type: 'error', message: t('otherError.queryNoBeEmpty', { ns: 'appDebug' }) }) + toast.error(t('otherError.queryNoBeEmpty', { ns: 'appDebug' })) return } } } if (contextVarEmpty) { - notify({ type: 'error', message: t('feature.dataSet.queryVariable.contextVarNotEmpty', { ns: 'appDebug' }) }) + toast.error(t('feature.dataSet.queryVariable.contextVarNotEmpty', { ns: 'appDebug' })) return } const postDatasets = dataSets.map(({ id }) => ({ @@ -851,7 +848,7 @@ const Configuration: FC = () => { modelConfig: newModelConfig, completionParams, }) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) + toast.success(t('api.success', { ns: 'common' })) setCanReturnToSimpleMode(false) return true diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index dd7a0c6a6c..1c9adca1d1 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -11,9 +11,9 @@ import Button from '@/app/components/base/button' import EmojiPicker from '@/app/components/base/emoji-picker' import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation' import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education' -import Modal from '@/app/components/base/modal' -import { SimpleSelect } from '@/app/components/base/select' -import { useToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/app/components/base/ui/select' +import { toast } from '@/app/components/base/ui/toast' import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector' import { useDocLink, useLocale } from '@/context/i18n' import { LanguagesSupported } from '@/i18n-config/language' @@ -39,7 +39,6 @@ const ExternalDataToolModal: FC = ({ }) => { const { t } = useTranslation() const docLink = useDocLink() - const { notify } = useToastContext() const locale = useLocale() const [localeData, setLocaleData] = useState(data.type ? data : { ...data, type: 'api' }) const [showEmojiPicker, setShowEmojiPicker] = useState(false) @@ -133,37 +132,34 @@ const ExternalDataToolModal: FC = ({ const handleSave = () => { if (!localeData.type) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.toolType.title', { ns: 'appDebug' }) }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.toolType.title', { ns: 'appDebug' }) })) return } if (!localeData.label) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.name.title', { ns: 'appDebug' }) }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.name.title', { ns: 'appDebug' }) })) return } if (!localeData.variable) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) })) return } if (localeData.variable && !/^[a-z_]\w{0,29}$/i.test(localeData.variable)) { - notify({ type: 'error', message: t('varKeyError.notValid', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) }) }) + toast.error(t('varKeyError.notValid', { ns: 'appDebug', key: t('feature.tools.modal.variableName.title', { ns: 'appDebug' }) })) return } if (localeData.type === 'api' && !localeData.config?.api_based_extension_id) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' })) return } if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) { for (let i = 0; i < currentProvider.form_schema.length; i++) { if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) { - notify({ - type: 'error', - message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }), - }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] })) return } } @@ -180,122 +176,128 @@ const ExternalDataToolModal: FC = ({ const action = data.type ? t('operation.edit', { ns: 'common' }) : t('operation.add', { ns: 'common' }) return ( - -
- {`${action} ${t('variableConfig.apiBasedVar', { ns: 'appDebug' })}`} -
-
-
- {t('apiBasedExtension.type', { ns: 'common' })} + +
+ {`${action} ${t('variableConfig.apiBasedVar', { ns: 'appDebug' })}`}
- { - return { - value: option.key, - name: option.name, - } - })} - onSelect={item => handleDataTypeChange(item.value as string)} - /> -
-
-
- {t('feature.tools.modal.name.title', { ns: 'appDebug' })} +
+
+ {t('apiBasedExtension.type', { ns: 'common' })} +
+
-
- handleValueChange({ label: e.target.value })} - className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none" - placeholder={t('feature.tools.modal.name.placeholder', { ns: 'appDebug' }) || ''} - /> - { setShowEmojiPicker(true) }} - className="!h-9 !w-9 cursor-pointer rounded-lg border-[0.5px] border-components-panel-border" - icon={localeData.icon} - background={localeData.icon_background} - /> -
-
-
-
- {t('feature.tools.modal.variableName.title', { ns: 'appDebug' })} -
- handleValueChange({ variable: e.target.value })} - className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none" - placeholder={t('feature.tools.modal.variableName.placeholder', { ns: 'appDebug' }) || ''} - /> -
- { - localeData.type === 'api' && ( -
-
- {t('apiBasedExtension.selector.title', { ns: 'common' })} - - - {t('apiBasedExtension.link', { ns: 'common' })} - -
- +
+ {t('feature.tools.modal.name.title', { ns: 'appDebug' })} +
+
+ handleValueChange({ label: e.target.value })} + className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none" + placeholder={t('feature.tools.modal.name.placeholder', { ns: 'appDebug' }) || ''} + /> + { setShowEmojiPicker(true) }} + className="!h-9 !w-9 cursor-pointer rounded-lg border-[0.5px] border-components-panel-border" + icon={localeData.icon} + background={localeData.icon_background} />
- ) - } - { - systemTypes.findIndex(t => t === localeData.type) < 0 - && currentProvider?.form_schema - && ( - +
+
+ {t('feature.tools.modal.variableName.title', { ns: 'appDebug' })} +
+ handleValueChange({ variable: e.target.value })} + className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-components-input-text-filled outline-none" + placeholder={t('feature.tools.modal.variableName.placeholder', { ns: 'appDebug' }) || ''} /> - ) - } -
- - -
- { - showEmojiPicker && ( - { - handleValueChange({ icon, icon_background }) - setShowEmojiPicker(false) - }} - onClose={() => { - handleValueChange({ icon: '', icon_background: '' }) - setShowEmojiPicker(false) - }} - /> - ) - } - +
+ { + localeData.type === 'api' && ( +
+
+ {t('apiBasedExtension.selector.title', { ns: 'common' })} + + + {t('apiBasedExtension.link', { ns: 'common' })} + +
+ +
+ ) + } + { + systemTypes.findIndex(t => t === localeData.type) < 0 + && currentProvider?.form_schema + && ( + + ) + } +
+ + +
+ { + showEmojiPicker && ( + { + handleValueChange({ icon, icon_background }) + setShowEmojiPicker(false) + }} + onClose={() => { + handleValueChange({ icon: '', icon_background: '' }) + setShowEmojiPicker(false) + }} + /> + ) + } + + ) } diff --git a/web/app/components/app/configuration/tools/index.tsx b/web/app/components/app/configuration/tools/index.tsx index 51a9e87a97..8ab71c73cf 100644 --- a/web/app/components/app/configuration/tools/index.tsx +++ b/web/app/components/app/configuration/tools/index.tsx @@ -5,7 +5,6 @@ import { RiDeleteBinLine, } from '@remixicon/react' import copy from 'copy-to-clipboard' -// abandoned import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' @@ -15,14 +14,17 @@ import { } from '@/app/components/base/icons/src/vender/line/general' import { Tool03 } from '@/app/components/base/icons/src/vender/solid/general' import Switch from '@/app/components/base/switch' -import { useToastContext } from '@/app/components/base/toast/context' -import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import ConfigContext from '@/context/debug-configuration' import { useModalContext } from '@/context/modal-context' const Tools = () => { const { t } = useTranslation() - const { notify } = useToastContext() const { setShowExternalDataToolModal } = useModalContext() const { externalDataToolsConfig, @@ -48,7 +50,7 @@ const Tools = () => { const promptVariables = modelConfig?.configs?.prompt_variables || [] for (let i = 0; i < promptVariables.length; i++) { if (promptVariables[i].key === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) return false } } @@ -66,7 +68,7 @@ const Tools = () => { for (let i = 0; i < existedExternalDataTools.length; i++) { if (existedExternalDataTools[i].variable === newExternalDataTool.variable) { - notify({ type: 'error', message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: existedExternalDataTools[i].variable }) }) + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: existedExternalDataTools[i].variable })) return false } } @@ -110,13 +112,14 @@ const Tools = () => {
{t('feature.tools.title', { ns: 'appDebug' })}
- + } /> +
{t('feature.tools.tips', { ns: 'appDebug' })}
- )} - /> +
+
{ !expanded && !!externalDataToolsConfig.length && ( @@ -151,18 +154,23 @@ const Tools = () => { background={item.icon_background} />
{item.label}
- -
{ - copy(item.variable || '') - setCopied(true) - }} - > - {item.variable} -
+ + { + copy(item.variable || '') + setCopied(true) + }} + > + {item.variable} +
+ )} + /> + + {copied ? t('copied', { ns: 'appApi' }) : `${item.variable}, ${t('copy', { ns: 'appApi' })}`} +
({ default: () => ({ theme: 'light' }), })) +const mockToast = { + success: (message: string, options?: Record) => mockNotify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => mockNotify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => mockNotify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => mockNotify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +} + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) const mockUseRouter = vi.mocked(useRouter) const mockPush = vi.fn() const mockCreateApp = vi.mocked(createApp) @@ -79,9 +91,8 @@ const renderModal = () => { const onClose = vi.fn() const onSuccess = vi.fn() render( - - - , + , + ) return { onClose, onSuccess } } diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index d7438d8c32..6cca4ae4a7 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -6,8 +6,8 @@ import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon import { useDebounceFn, useKeyPress } from 'ahooks' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' - import { trackEvent } from '@/app/components/base/amplitude' + import AppIcon from '@/app/components/base/app-icon' import Badge from '@/app/components/base/badge' import Button from '@/app/components/base/button' 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 d626acf8c4..7d057334ba 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -2,15 +2,13 @@ import type { DocPathWithoutLang } from '@/types/doc-paths' import { useKeyPress } from 'ahooks' -import { noop } from 'es-toolkit/function' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { trackEvent } from '@/app/components/base/amplitude' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' -import Modal from '@/app/components/base/modal' -import { ToastContext } from '@/app/components/base/toast/context' +import { Dialog, DialogContent } from '@/app/components/base/ui/dialog' +import { toast } from '@/app/components/base/ui/toast' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' @@ -58,7 +56,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS const { push } = useRouter() const { t } = useTranslation() const docLink = useDocLink() - const { notify } = useContext(ToastContext) const [currentFile, setDSLFile] = useState(droppedFile) const [fileContent, setFileContent] = useState() const [currentTab, setCurrentTab] = useState(activeTab) @@ -152,11 +149,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS if (onClose) onClose() - notify({ - type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning', - message: t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }), - children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }), - }) + toast(t(status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution', { ns: 'app' }), { type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning', description: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('newApp.appCreateDSLWarning', { ns: 'app' }) }) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') if (app_id) await handleCheckPluginDependencies(app_id) @@ -173,12 +166,12 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS setImportId(id) } else { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } // eslint-disable-next-line unused-imports/no-unused-vars catch (e) { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } finally { isCreatingRef.current = false @@ -213,22 +206,19 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS if (onClose) onClose() - notify({ - type: 'success', - message: t('newApp.appCreated', { ns: 'app' }), - }) + toast.success(t('newApp.appCreated', { ns: 'app' })) if (app_id) await handleCheckPluginDependencies(app_id) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push) } else if (status === DSLImportStatus.FAILED) { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } // eslint-disable-next-line unused-imports/no-unused-vars catch (e) { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } @@ -265,94 +255,98 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS return ( <> - { + if (!open) + onClose() + }} > -
- {t('importApp', { ns: 'app' })} -
onClose()} - > -
-
- { - tabs.map(tab => ( -
setCurrentTab(tab.key)} - > - {tab.label} - {currentTab === tab.key && ( -
- )} -
- )) - } -
-
- {currentTab === CreateFromDSLModalTab.FROM_FILE && ( - - )} - {currentTab === CreateFromDSLModalTab.FROM_URL && ( -
-
- {t('importFromDSLUrl', { ns: 'app' })} -
- setDslUrlValue(e.target.value)} +
+ { + tabs.map(tab => ( +
setCurrentTab(tab.key)} + > + {tab.label} + {currentTab === tab.key && ( +
+ )} +
+ )) + } +
+
+ {currentTab === CreateFromDSLModalTab.FROM_FILE && ( + + )} + {currentTab === CreateFromDSLModalTab.FROM_URL && ( +
+
+ {t('importFromDSLUrl', { ns: 'app' })} +
+ setDslUrlValue(e.target.value)} + /> +
+ )} +
+ {isAppsFull && ( +
+
)} -
- {isAppsFull && ( -
- -
- )} -
- - {learnMoreLabel} - -
- - + {learnMoreLabel} + +
+ + +
-
- + + {showErrorModal && ( = ({ displayName = 'YAML', }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [dragging, setDragging] = useState(false) const dropRef = useRef(null) const dragRef = useRef(null) @@ -60,7 +58,7 @@ const Uploader: FC = ({ return const files = Array.from(e.dataTransfer.files) if (files.length > 1) { - notify({ type: 'error', message: t('stepOne.uploader.validation.count', { ns: 'datasetCreation' }) }) + toast.error(t('stepOne.uploader.validation.count', { ns: 'datasetCreation' })) return } updateFile(files[0]) diff --git a/web/app/components/app/duplicate-modal/index.spec.tsx b/web/app/components/app/duplicate-modal/index.spec.tsx index ef12646571..e70329a105 100644 --- a/web/app/components/app/duplicate-modal/index.spec.tsx +++ b/web/app/components/app/duplicate-modal/index.spec.tsx @@ -2,7 +2,7 @@ import type { ProviderContextState } from '@/context/provider-context' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { Plan } from '@/app/components/billing/type' import { baseProviderContextValue } from '@/context/provider-context' import DuplicateAppModal from './index' @@ -129,7 +129,7 @@ describe('DuplicateAppModal', () => { it('should show error toast when name is empty', async () => { const user = userEvent.setup() - const toastSpy = vi.spyOn(Toast, 'notify') + const toastSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error') // Arrange const { onConfirm, onHide } = renderComponent() @@ -138,7 +138,7 @@ describe('DuplicateAppModal', () => { await user.click(screen.getByRole('button', { name: 'app.duplicate' })) // Assert - expect(toastSpy).toHaveBeenCalledWith({ type: 'error', message: 'explore.appCustomize.nameRequired' }) + expect(toastSpy).toHaveBeenCalledWith('explore.appCustomize.nameRequired') expect(onConfirm).not.toHaveBeenCalled() expect(onHide).not.toHaveBeenCalled() }) diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index a698c6664e..e74c6e1865 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -9,7 +9,7 @@ import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { useProviderContext } from '@/context/provider-context' import { cn } from '@/utils/classnames' @@ -57,7 +57,7 @@ const DuplicateAppModal = ({ const submit = () => { if (!name.trim()) { - Toast.notify({ type: 'error', message: t('appCustomize.nameRequired', { ns: 'explore' }) }) + toast.error(t('appCustomize.nameRequired', { ns: 'explore' })) return } onConfirm({ diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 453c7c9d4c..4a22a0c85f 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -30,8 +30,8 @@ import Drawer from '@/app/components/base/drawer' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import Loading from '@/app/components/base/loading' import MessageLogModal from '@/app/components/base/message-log-modal' -import { ToastContext } from '@/app/components/base/toast/context' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { WorkflowContextProvider } from '@/app/components/workflow/context' import { useAppContext } from '@/context/app-context' @@ -223,7 +223,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { const { userProfile: { timezone } } = useAppContext() const { formatTime } = useTimestamp() const { onClose, appDetail } = useContext(DrawerContext) - const { notify } = useContext(ToastContext) const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow((state: AppStoreState) => ({ currentLogItem: state.currentLogItem, setCurrentLogItem: state.setCurrentLogItem, @@ -413,14 +412,14 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { return item })) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } - }, [allChatItems, appDetail?.id, notify, t]) + }, [allChatItems, appDetail?.id, t]) const fetchInitiated = useRef(false) @@ -734,7 +733,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => { // Text Generator App Session Details Including Message List const { data: conversationDetail, refetch: conversationDetailMutate } = useCompletionConversationDetail(appId, conversationId) - const { notify } = useContext(ToastContext) const { t } = useTranslation() const handleFeedback = async (mid: string, { rating, content }: FeedbackType): Promise => { @@ -744,11 +742,11 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st body: { message_id: mid, rating, content: content ?? undefined }, }) conversationDetailMutate() - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } } @@ -757,11 +755,11 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st try { await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } }) conversationDetailMutate() - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } } @@ -783,7 +781,6 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st */ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => { const { data: conversationDetail } = useChatConversationDetail(appId, conversationId) - const { notify } = useContext(ToastContext) const { t } = useTranslation() const handleFeedback = async (mid: string, { rating, content }: FeedbackType): Promise => { @@ -792,11 +789,11 @@ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string } url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating, content: content ?? undefined }, }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } } @@ -804,11 +801,11 @@ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string } const handleAnnotation = async (mid: string, value: string): Promise => { try { await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } }) - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) return true } catch { - notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) return false } } diff --git a/web/app/components/app/overview/settings/index.spec.tsx b/web/app/components/app/overview/settings/index.spec.tsx index b849b4f015..95259e72e0 100644 --- a/web/app/components/app/overview/settings/index.spec.tsx +++ b/web/app/components/app/overview/settings/index.spec.tsx @@ -33,6 +33,19 @@ vi.mock('react-i18next', async () => { }) const mockNotify = vi.fn() +const mockToast = { + success: (message: string, options?: Record) => mockNotify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => mockNotify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => mockNotify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => mockNotify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), +} + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) const mockOnClose = vi.fn() const mockOnSave = vi.fn() const mockSetShowPricingModal = vi.fn() @@ -59,13 +72,6 @@ vi.mock('@/context/modal-context', () => ({ useModalContext: () => buildModalContext(), })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ - notify: mockNotify, - close: vi.fn(), - }), -})) - vi.mock('@/context/i18n', async () => { const actual = await vi.importActual('@/context/i18n') return { diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 13dacde424..0d77d32ec4 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -19,8 +19,8 @@ import PremiumBadge from '@/app/components/base/premium-badge' import { SimpleSelect } from '@/app/components/base/select' import Switch from '@/app/components/base/switch' import Textarea from '@/app/components/base/textarea' -import { useToastContext } from '@/app/components/base/toast/context' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' @@ -65,7 +65,6 @@ const SettingsModal: FC = ({ onClose, onSave, }) => { - const { notify } = useToastContext() const [isShowMore, setIsShowMore] = useState(false) const { title, @@ -159,7 +158,7 @@ const SettingsModal: FC = ({ const onClickSave = async () => { if (!inputInfo.title) { - notify({ type: 'error', message: t('newApp.nameNotEmpty', { ns: 'app' }) }) + toast.error(t('newApp.nameNotEmpty', { ns: 'app' })) return } @@ -181,11 +180,11 @@ const SettingsModal: FC = ({ if (inputInfo !== null) { if (!validateColorHex(inputInfo.chatColorTheme)) { - notify({ type: 'error', message: t(`${prefixSettings}.invalidHexMessage`, { ns: 'appOverview' }) }) + toast.error(t(`${prefixSettings}.invalidHexMessage`, { ns: 'appOverview' })) return } if (!validatePrivacyPolicy(inputInfo.privacyPolicy)) { - notify({ type: 'error', message: t(`${prefixSettings}.invalidPrivacyPolicy`, { ns: 'appOverview' }) }) + toast.error(t(`${prefixSettings}.invalidPrivacyPolicy`, { ns: 'appOverview' })) return } } diff --git a/web/app/components/app/switch-app-modal/index.spec.tsx b/web/app/components/app/switch-app-modal/index.spec.tsx index 67c4c36e23..4b5324afb5 100644 --- a/web/app/components/app/switch-app-modal/index.spec.tsx +++ b/web/app/components/app/switch-app-modal/index.spec.tsx @@ -3,7 +3,6 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { useStore as useAppStore } from '@/app/components/app/store' -import { ToastContext } from '@/app/components/base/toast/context' import { Plan } from '@/app/components/billing/type' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { AppModeEnum } from '@/types/app' @@ -111,20 +110,32 @@ const createMockApp = (overrides: Partial = {}): App => ({ const renderComponent = (overrides: Partial> = {}) => { const notify = vi.fn() + const mockToast = { + success: (message: string, options?: Record) => notify({ type: 'success', message, ...options }), + error: (message: string, options?: Record) => notify({ type: 'error', message, ...options }), + warning: (message: string, options?: Record) => notify({ type: 'warning', message, ...options }), + info: (message: string, options?: Record) => notify({ type: 'info', message, ...options }), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + } + + vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, + })) const onClose = vi.fn() const onSuccess = vi.fn() const appDetail = createMockApp() const utils = render( - - - , + , + ) return { diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index 7c3269d52c..ffa5dc6ef4 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -5,7 +5,6 @@ import { RiCloseLine } from '@remixicon/react' import { noop } from 'es-toolkit/function' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useStore as useAppStore } from '@/app/components/app/store' import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' @@ -14,7 +13,7 @@ import Confirm from '@/app/components/base/confirm' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import AppsFull from '@/app/components/billing/apps-full-in-dialog' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' @@ -37,7 +36,6 @@ type SwitchAppModalProps = { const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClose }: SwitchAppModalProps) => { const { push, replace } = useRouter() const { t } = useTranslation() - const { notify } = useContext(ToastContext) const setAppDetail = useAppStore(s => s.setAppDetail) const { isCurrentWorkspaceEditor } = useAppContext() @@ -68,7 +66,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo onSuccess() if (onClose) onClose() - notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }) }) + toast.success(t('newApp.appCreated', { ns: 'app' })) if (inAppDetail) setAppDetail() if (removeOriginal) @@ -84,7 +82,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo ) } catch { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index 7081731cba..d032054796 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -28,7 +28,7 @@ import { useChatContext } from '@/app/components/base/chat/chat/context' import Loading from '@/app/components/base/loading' import { Markdown } from '@/app/components/base/markdown' import NewAudioButton from '@/app/components/base/new-audio-button' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useParams } from '@/next/navigation' import { fetchTextGenerationMessage } from '@/service/debug' import { AppSourceType, fetchMoreLikeThis, submitHumanInputForm, updateFeedback } from '@/service/share' @@ -145,7 +145,7 @@ const GenerationItem: FC = ({ const handleMoreLikeThis = async () => { if (isQuerying || !messageId) { - Toast.notify({ type: 'warning', message: t('errorMessage.waitForResponse', { ns: 'appDebug' }) }) + toast.warning(t('errorMessage.waitForResponse', { ns: 'appDebug' })) return } startQuerying() @@ -368,7 +368,7 @@ const GenerationItem: FC = ({ copy(copyContent) else copy(JSON.stringify(copyContent)) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.copySuccessfully', { ns: 'common' })) }} > diff --git a/web/app/components/app/text-generate/saved-items/index.spec.tsx b/web/app/components/app/text-generate/saved-items/index.spec.tsx index b45a1cca6c..dff0950f89 100644 --- a/web/app/components/app/text-generate/saved-items/index.spec.tsx +++ b/web/app/components/app/text-generate/saved-items/index.spec.tsx @@ -4,7 +4,7 @@ import copy from 'copy-to-clipboard' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import SavedItems from './index' vi.mock('copy-to-clipboard', () => ({ @@ -16,7 +16,7 @@ vi.mock('@/next/navigation', () => ({ })) const mockCopy = vi.mocked(copy) -const toastNotifySpy = vi.spyOn(Toast, 'notify') +const toastSuccessSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success') const baseProps: ISavedItemsProps = { list: [ @@ -30,7 +30,7 @@ const baseProps: ISavedItemsProps = { describe('SavedItems', () => { beforeEach(() => { vi.clearAllMocks() - toastNotifySpy.mockClear() + toastSuccessSpy.mockClear() }) it('renders saved answers with metadata and controls', () => { @@ -58,7 +58,7 @@ describe('SavedItems', () => { fireEvent.click(copyButton) expect(mockCopy).toHaveBeenCalledWith('hello world') - expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'success', message: 'common.actionMsg.copySuccessfully' }) + expect(toastSuccessSpy).toHaveBeenCalledWith('common.actionMsg.copySuccessfully') fireEvent.click(deleteButton) expect(handleRemove).toHaveBeenCalledWith('1') 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 9e54d9abee..620de7eb55 100644 --- a/web/app/components/app/text-generate/saved-items/index.tsx +++ b/web/app/components/app/text-generate/saved-items/index.tsx @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { Markdown } from '@/app/components/base/markdown' import NewAudioButton from '@/app/components/base/new-audio-button' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { cn } from '@/utils/classnames' import NoData from './no-data' @@ -60,7 +60,7 @@ const SavedItems: FC = ({ {isShowTextToSpeech && } { copy(answer) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.copySuccessfully', { ns: 'common' })) }} > diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index c228588670..2983f80f58 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -1,24 +1,18 @@ 'use client' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' -import type { HtmlContentProps } from '@/app/components/base/popover' import type { Tag } from '@/app/components/base/tag-management/constant' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' import type { WorkflowOnlineUser } from '@/models/app' import type { App } from '@/types/app' import * as React from 'react' -import { useCallback, useEffect, useMemo, useState, useTransition } from 'react' +import { useCallback, useMemo, useState, useTransition } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' import Divider from '@/app/components/base/divider' -import CustomPopover from '@/app/components/base/popover' import TagSelector from '@/app/components/base/tag-management/selector' -import Toast from '@/app/components/base/toast' -import { ToastContext } from '@/app/components/base/toast/context' -import Tooltip from '@/app/components/base/tooltip' import { AlertDialog, AlertDialogActions, @@ -28,6 +22,17 @@ import { AlertDialogDescription, AlertDialogTitle, } from '@/app/components/base/ui/alert-dialog' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/app/components/base/ui/popover' +import { toast } from '@/app/components/base/ui/toast' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' @@ -65,6 +70,166 @@ const AccessControl = dynamic(() => import('@/app/components/app/app-access-cont ssr: false, }) +type AppCardOperationsProps = { + app: App + webappAuthEnabled: boolean + isCurrentWorkspaceEditor: boolean + exporting: boolean + secretEnvListLength: number + isUpgradingRuntime: boolean + popupClassName: string + onEdit: () => void + onDuplicate: () => void + onExport: () => void + onSwitch: () => void + onDelete: () => void + onAccessControl: () => void + onInstalledApp: () => void + onUpgradeRuntime: () => void +} + +const AppCardOperations = ({ + app, + webappAuthEnabled, + isCurrentWorkspaceEditor, + exporting, + secretEnvListLength, + isUpgradingRuntime, + popupClassName, + onEdit, + onDuplicate, + onExport, + onSwitch, + onDelete, + onAccessControl, + onInstalledApp, + onUpgradeRuntime, +}: AppCardOperationsProps) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({ + appId: app.id, + enabled: !!open && webappAuthEnabled, + }) + + const onMouseLeave = () => { + setOpen(false) + } + + const onClickInstalledApp = async () => { + onInstalledApp() + onMouseLeave() + } + + const onClickUpgradeRuntime = async () => { + onUpgradeRuntime() + onMouseLeave() + } + + return ( + + + {t('operation.more', { ns: 'common' })} + + + )} + /> + +
+ + + + + {(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && ( + <> + + + + )} + { + !app.has_draft_trigger && ( + (!webappAuthEnabled) + ? ( + <> + + + + ) + : !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && ( + <> + + + + ) + ) + } + + { + webappAuthEnabled && isCurrentWorkspaceEditor && ( + <> + + + + ) + } + {app.runtime_type !== 'sandboxed' + && (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT) + && ( + + )} + +
+
+
+ ) +} + export type AppCardProps = { app: App onRefresh?: () => void @@ -73,7 +238,6 @@ export type AppCardProps = { const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) const { isCurrentWorkspaceEditor } = useAppContext() const { onPlanInfoChanged } = useProviderContext() @@ -93,20 +257,17 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { const onConfirmDelete = useCallback(async () => { try { await mutateDeleteApp(app.id) - notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) }) + toast.success(t('appDeleted', { ns: 'app' })) onPlanInfoChanged() } catch (e: unknown) { - notify({ - type: 'error', - message: `${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error ? `: ${e.message}` : ''}`, - }) + toast.error(`${t('appDeleteFailed', { ns: 'app' })}${e instanceof Error ? `: ${e.message}` : ''}`) } finally { setShowConfirmDelete(false) setConfirmDeleteInput('') } - }, [app.id, mutateDeleteApp, notify, onPlanInfoChanged, t]) + }, [app.id, mutateDeleteApp, onPlanInfoChanged, t]) const onDeleteDialogOpenChange = useCallback((open: boolean) => { if (isDeleting) @@ -138,20 +299,14 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { max_active_requests, }) setShowEditModal(false) - notify({ - type: 'success', - message: t('editDone', { ns: 'app' }), - }) + toast.success(t('editDone', { ns: 'app' })) if (onRefresh) onRefresh() } catch (e: unknown) { - notify({ - type: 'error', - message: (e instanceof Error ? e.message : '') || t('editFailed', { ns: 'app' }), - }) + toast.error((e instanceof Error ? e.message : '') || t('editFailed', { ns: 'app' })) } - }, [app.id, notify, onRefresh, t]) + }, [app.id, onRefresh, t]) const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { try { @@ -164,10 +319,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { mode: app.mode, }) setShowDuplicateModal(false) - notify({ - type: 'success', - message: t('newApp.appCreated', { ns: 'app' }), - }) + toast.success(t('newApp.appCreated', { ns: 'app' })) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') if (onRefresh) onRefresh() @@ -175,7 +327,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { getRedirection(isCurrentWorkspaceEditor, newApp, push) } catch { - notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.error(t('newApp.appCreateFailed', { ns: 'app' })) } } @@ -197,10 +349,7 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { downloadBlob({ data: file, fileName: `${app.name}.yml` }) } catch { - notify({ - type: 'error', - message: t('exportFailed', { ns: 'app' }), - }) + toast.error(t('exportFailed', { ns: 'app' })) } } @@ -219,196 +368,71 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => { setSecretEnvList(list) } catch { - notify({ - type: 'error', - message: t('exportFailed', { ns: 'app' }), - }) + toast.error(t('exportFailed', { ns: 'app' })) } } + const [isUpgradingRuntime, startUpgradeRuntime] = useTransition() + const onSwitch = () => { if (onRefresh) onRefresh() setShowSwitchModal(false) } - const [isUpgradingRuntime, startUpgradeRuntime] = useTransition() - const isClassicWorkflowApp = app.runtime_type !== 'sandboxed' - && (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT) - const onUpdateAccessControl = useCallback(() => { if (onRefresh) onRefresh() setShowAccessControl(false) }, [onRefresh, setShowAccessControl]) - const Operations = (props: HtmlContentProps) => { - const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp } = useGetUserCanAccessApp({ appId: app?.id, enabled: (!!props?.open && systemFeatures.webapp_auth.enabled) }) - const onMouseLeave = async () => { - props.onClose?.() - } - const onClickSettings = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowEditModal(true) - } - const onClickDuplicate = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowDuplicateModal(true) - } - const onClickExport = (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - startExport(async () => { - await exportCheck() + const handleOpenEditModal = useCallback(() => setShowEditModal(true), []) + const handleOpenDuplicateModal = useCallback(() => setShowDuplicateModal(true), []) + const handleOpenSwitchModal = useCallback(() => setShowSwitchModal(true), []) + const handleOpenDeleteModal = useCallback(() => setShowConfirmDelete(true), []) + const handleOpenAccessControl = useCallback(() => setShowAccessControl(true), []) + const handleExport = useCallback(() => { + startExport(async () => { + await exportCheck() + }) + }, [exportCheck, startExport]) + const handleInstalledApp = useCallback(async () => { + try { + await openAsyncWindow(async () => { + const { installed_apps } = (await fetchInstalledAppList(app.id) || {}) as { installed_apps?: { id: string }[] } + if (installed_apps && installed_apps.length > 0) + return `${basePath}/explore/installed/${installed_apps[0].id}` + throw new Error('No app found in Explore') + }, { + onError: (err) => { + toast.error(`${err.message || err}`) + }, }) } - const onClickSwitch = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowSwitchModal(true) + catch (e: unknown) { + toast.error(e instanceof Error ? e.message : String(e)) } - const onClickDelete = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowConfirmDelete(true) - } - const onClickAccessControl = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - setShowAccessControl(true) - } - const onClickInstalledApp = async (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() + }, [app.id, openAsyncWindow]) + const handleUpgradeRuntime = useCallback(() => { + startUpgradeRuntime(async () => { try { - await openAsyncWindow(async () => { - const { installed_apps } = (await fetchInstalledAppList(app.id) || {}) as { installed_apps?: { id: string }[] } - if (installed_apps && installed_apps.length > 0) - return `${basePath}/explore/installed/${installed_apps[0].id}` - throw new Error('No app found in Explore') - }, { - onError: (err) => { - Toast.notify({ type: 'error', message: `${err.message || err}` }) - }, - }) + const res = await upgradeAppRuntime(app.id) + if (res.result === 'success' && res.new_app_id) { + toast.success(t('sandboxMigrationModal.upgrade', { ns: 'workflow' })) + const params = new URLSearchParams({ + upgraded_from: app.id, + upgraded_from_name: app.name, + }) + push(`/app/${res.new_app_id}/workflow?${params.toString()}`) + } } catch (e: unknown) { - Toast.notify({ type: 'error', message: e instanceof Error ? e.message : String(e) }) + toast.error((e instanceof Error ? e.message : '') || 'Upgrade failed') } - } - const onClickUpgradeRuntime = (e: React.MouseEvent) => { - e.stopPropagation() - props.onClick?.() - e.preventDefault() - startUpgradeRuntime(async () => { - try { - const res = await upgradeAppRuntime(app.id) - if (res.result === 'success' && res.new_app_id) { - notify({ type: 'success', message: t('sandboxMigrationModal.upgrade', { ns: 'workflow' }) }) - const params = new URLSearchParams({ - upgraded_from: app.id, - upgraded_from_name: app.name, - }) - push(`/app/${res.new_app_id}/workflow?${params.toString()}`) - } - } - catch (e: unknown) { - notify({ type: 'error', message: (e instanceof Error ? e.message : '') || 'Upgrade failed' }) - } - }) - } - return ( -
- - - - - {(app.mode === AppModeEnum.COMPLETION || app.mode === AppModeEnum.CHAT) && ( - <> - - - - )} - { - !app.has_draft_trigger && ( - (!systemFeatures.webapp_auth.enabled) - ? ( - <> - - - - ) - : !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && ( - <> - - - - ) - ) - } - - { - systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && ( - <> - - - - ) - } - {isClassicWorkflowApp && ( - - )} - -
- ) - } + }) + }, [app.id, app.name, push, startUpgradeRuntime, t]) - const [tags, setTags] = useState(app.tags) - useEffect(() => { - setTags(app.tags) - }, [app.tags]) + const [tags, setTags] = useState(() => app.tags) const EditTimeText = useMemo(() => { const timeText = formatTime({ @@ -463,23 +487,27 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
{app.access_mode === AccessMode.PUBLIC && ( - - + + } /> + {t('accessItemsDescription.anyone', { ns: 'app' })} )} {app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && ( - - + + } /> + {t('accessItemsDescription.specific', { ns: 'app' })} )} {app.access_mode === AccessMode.ORGANIZATION && ( - - + + } /> + {t('accessItemsDescription.organization', { ns: 'app' })} )} {app.access_mode === AccessMode.EXTERNAL_MEMBERS && ( - - + + } /> + {t('accessItemsDescription.external', { ns: 'app' })} )}
@@ -521,29 +549,26 @@ const AppCard = ({ app, onRefresh, onlineUsers = [] }: AppCardProps) => {
- } - position="br" - trigger="click" - btnElement={( -
- {t('operation.more', { ns: 'common' })} - -
- )} - btnClassName={open => - cn( - open ? '!bg-state-base-hover !shadow-none' : '!bg-transparent', - 'h-8 w-8 rounded-md border-none !p-2 hover:!bg-state-base-hover', - )} +
diff --git a/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx index 8b796435e0..6ce1e54a47 100644 --- a/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/detail.spec.tsx @@ -1,16 +1,32 @@ -import type { ComponentProps } from 'react' +import type { ComponentProps, ReactNode } from 'react' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentLogDetailResponse } from '@/models/log' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { useStore as useAppStore } from '@/app/components/app/store' -import { ToastContext } from '@/app/components/base/toast/context' import { fetchAgentLogDetail } from '@/service/log' import AgentLogDetail from '../detail' +const { mockToast } = vi.hoisted(() => { + const mockToast = Object.assign(vi.fn(), { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + }) + return { mockToast } +}) + vi.mock('@/service/log', () => ({ fetchAgentLogDetail: vi.fn(), })) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) + vi.mock('@/app/components/app/store', () => ({ useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })), })) @@ -22,7 +38,7 @@ vi.mock('@/app/components/workflow/run/status', () => ({ })) vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({ - default: ({ title, value }: { title: React.ReactNode, value: string | object }) => ( + default: ({ title, value }: { title: ReactNode, value: string | object }) => (
{title} {typeof value === 'string' ? value : JSON.stringify(value)} @@ -76,19 +92,13 @@ const createMockResponse = (overrides: Partial = {}): Ag }) describe('AgentLogDetail', () => { - const notify = vi.fn() - const renderComponent = (props: Partial> = {}) => { const defaultProps: ComponentProps = { conversationID: 'conv-id', messageID: 'msg-id', log: createMockLog(), } - return render( - ['value']}> - - , - ) + return render() } const renderAndWaitForData = async (props: Partial> = {}) => { @@ -212,10 +222,7 @@ describe('AgentLogDetail', () => { renderComponent() await waitFor(() => { - expect(notify).toHaveBeenCalledWith({ - type: 'error', - message: 'Error: API Error', - }) + expect(mockToast.error).toHaveBeenCalledWith('Error: API Error') }) }) diff --git a/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx b/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx index b2db524453..d1581c40b5 100644 --- a/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx +++ b/web/app/components/base/agent-log-modal/__tests__/index.spec.tsx @@ -1,14 +1,30 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { useClickAway } from 'ahooks' -import { ToastContext } from '@/app/components/base/toast/context' import { fetchAgentLogDetail } from '@/service/log' import AgentLogModal from '../index' +const { mockToast } = vi.hoisted(() => { + const mockToast = Object.assign(vi.fn(), { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + info: vi.fn(), + dismiss: vi.fn(), + update: vi.fn(), + promise: vi.fn(), + }) + return { mockToast } +}) + vi.mock('@/service/log', () => ({ fetchAgentLogDetail: vi.fn(), })) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: mockToast, +})) + vi.mock('@/app/components/app/store', () => ({ useStore: vi.fn(selector => selector({ appDetail: { id: 'app-id' } })), })) @@ -94,11 +110,7 @@ describe('AgentLogModal', () => { }) it('should render correctly when log item is provided', async () => { - render( - ['value']}> - - , - ) + render() expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument() @@ -110,11 +122,7 @@ describe('AgentLogModal', () => { it('should call onCancel when close button is clicked', () => { vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) - render( - ['value']}> - - , - ) + render() const closeBtn = screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i }).nextElementSibling! fireEvent.click(closeBtn) @@ -130,11 +138,7 @@ describe('AgentLogModal', () => { clickAwayHandler = callback }) - render( - ['value']}> - - , - ) + render() clickAwayHandler(new Event('click')) expect(mockProps.onCancel).toHaveBeenCalledTimes(1) @@ -150,11 +154,7 @@ describe('AgentLogModal', () => { } }) - render( - ['value']}> - - , - ) + render() expect(mockProps.onCancel).not.toHaveBeenCalled() }) diff --git a/web/app/components/base/agent-log-modal/detail.tsx b/web/app/components/base/agent-log-modal/detail.tsx index 21ed0be7e8..6550b305f8 100644 --- a/web/app/components/base/agent-log-modal/detail.tsx +++ b/web/app/components/base/agent-log-modal/detail.tsx @@ -7,10 +7,9 @@ import { flatten } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useStore as useAppStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { fetchAgentLogDetail } from '@/service/log' import { cn } from '@/utils/classnames' import ResultPanel from './result' @@ -22,28 +21,19 @@ export type AgentLogDetailProps = { log: IChatItem messageID: string } - -const AgentLogDetail: FC = ({ - activeTab = 'DETAIL', - conversationID, - messageID, - log, -}) => { +const AgentLogDetail: FC = ({ activeTab = 'DETAIL', conversationID, messageID, log }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [currentTab, setCurrentTab] = useState(activeTab) const appDetail = useAppStore(s => s.appDetail) const [loading, setLoading] = useState(true) const [runDetail, setRunDetail] = useState() const [list, setList] = useState([]) - const tools = useMemo(() => { const res = uniq(flatten(runDetail?.iterations.map((iteration) => { return iteration.tool_calls.map((tool: any) => tool.tool_name).filter(Boolean) })).filter(Boolean)) return res }, [runDetail]) - const getLogDetail = useCallback(async (appID: string, conversationID: string, messageID: string) => { try { const res = await fetchAgentLogDetail({ @@ -57,51 +47,30 @@ const AgentLogDetail: FC = ({ setList(res.iterations) } catch (err) { - notify({ - type: 'error', - message: `${err}`, - }) + toast.error(`${err}`) } - }, [notify]) - + }, []) const getData = async (appID: string, conversationID: string, messageID: string) => { setLoading(true) await getLogDetail(appID, conversationID, messageID) setLoading(false) } - const switchTab = async (tab: string) => { setCurrentTab(tab) } - useEffect(() => { // fetch data if (appDetail) getData(appDetail.id, conversationID, messageID) }, [appDetail, conversationID, messageID]) - return (
{/* tab */}
-
switchTab('DETAIL')} - > +
switchTab('DETAIL')}> {t('detail', { ns: 'runLog' })}
-
switchTab('TRACING')} - > +
switchTab('TRACING')}> {t('tracing', { ns: 'runLog' })}
@@ -112,29 +81,10 @@ const AgentLogDetail: FC = ({
)} - {!loading && currentTab === 'DETAIL' && runDetail && ( - - )} - {!loading && currentTab === 'TRACING' && ( - - )} + {!loading && currentTab === 'DETAIL' && runDetail && ()} + {!loading && currentTab === 'TRACING' && ()}
) } - export default AgentLogDetail diff --git a/web/app/components/base/agent-log-modal/index.stories.tsx b/web/app/components/base/agent-log-modal/index.stories.tsx index 87318848b4..e8b49600a5 100644 --- a/web/app/components/base/agent-log-modal/index.stories.tsx +++ b/web/app/components/base/agent-log-modal/index.stories.tsx @@ -3,7 +3,7 @@ import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentLogDetailResponse } from '@/models/log' import { useEffect, useRef } from 'react' import { useStore as useAppStore } from '@/app/components/app/store' -import { ToastProvider } from '@/app/components/base/toast' +import { ToastHost } from '@/app/components/base/ui/toast' import AgentLogModal from '.' const MOCK_RESPONSE: AgentLogDetailResponse = { @@ -109,7 +109,8 @@ const AgentLogModalDemo = ({ }, [setAppDetail]) return ( - + <> +
-
+ ) } diff --git a/web/app/components/base/audio-btn/__tests__/audio.spec.ts b/web/app/components/base/audio-btn/__tests__/audio.spec.ts index 00ffea2dfb..d898da4775 100644 --- a/web/app/components/base/audio-btn/__tests__/audio.spec.ts +++ b/web/app/components/base/audio-btn/__tests__/audio.spec.ts @@ -6,7 +6,7 @@ import AudioPlayer from '../audio' const mockToastNotify = vi.hoisted(() => vi.fn()) const mockTextToAudioStream = vi.hoisted(() => vi.fn()) -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: (...args: unknown[]) => mockToastNotify(...args), }, diff --git a/web/app/components/base/audio-btn/audio.ts b/web/app/components/base/audio-btn/audio.ts index abfcad7c2f..5afe2bb656 100644 --- a/web/app/components/base/audio-btn/audio.ts +++ b/web/app/components/base/audio-btn/audio.ts @@ -1,4 +1,4 @@ -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { AppSourceType, textToAudioStream } from '@/service/share' declare global { @@ -7,7 +7,6 @@ declare global { ManagedMediaSource: any } } - export default class AudioPlayer { mediaSource: MediaSource | null audio: HTMLAudioElement @@ -22,7 +21,6 @@ export default class AudioPlayer { url: string isPublic: boolean callback: ((event: string) => void) | null - constructor(streamUrl: string, isPublic: boolean, msgId: string | undefined, msgContent: string | null | undefined, voice: string | undefined, callback: ((event: string) => void) | null) { this.audioContext = new AudioContext() this.msgId = msgId @@ -31,14 +29,10 @@ export default class AudioPlayer { this.isPublic = isPublic this.voice = voice this.callback = callback - // Compatible with iphone ios17 ManagedMediaSource const MediaSource = window.ManagedMediaSource || window.MediaSource if (!MediaSource) { - Toast.notify({ - message: 'Your browser does not support audio streaming, if you are using an iPhone, please update to iOS 17.1 or later.', - type: 'error', - }) + toast.error('Your browser does not support audio streaming, if you are using an iPhone, please update to iOS 17.1 or later.') } this.mediaSource = MediaSource ? new MediaSource() : null this.audio = new Audio() @@ -49,7 +43,6 @@ export default class AudioPlayer { } this.audio.src = this.mediaSource ? URL.createObjectURL(this.mediaSource) : '' this.audio.autoplay = true - const source = this.audioContext.createMediaElementSource(this.audio) source.connect(this.audioContext.destination) this.listenMediaSource('audio/mpeg') @@ -63,7 +56,6 @@ export default class AudioPlayer { this.mediaSource?.addEventListener('sourceopen', () => { if (this.sourceBuffer) return - this.sourceBuffer = this.mediaSource?.addSourceBuffer(contentType) }) } @@ -106,22 +98,18 @@ export default class AudioPlayer { voice: this.voice, text: this.msgContent, }) - if (audioResponse.status !== 200) { this.isLoadData = false if (this.callback) this.callback('error') } - const reader = audioResponse.body.getReader() while (true) { const { value, done } = await reader.read() - if (done) { this.receiveAudioData(value) break } - this.receiveAudioData(value) } } @@ -167,7 +155,6 @@ export default class AudioPlayer { this.theEndOfStream() clearInterval(timer) } - if (this.cacheBuffers.length && !this.sourceBuffer?.updating) { const arrayBuffer = this.cacheBuffers.shift()! this.sourceBuffer?.appendBuffer(arrayBuffer) @@ -180,7 +167,6 @@ export default class AudioPlayer { this.finishStream() return } - const audioContent = Buffer.from(audio, 'base64') this.receiveAudioData(new Uint8Array(audioContent)) if (play) { @@ -196,7 +182,6 @@ export default class AudioPlayer { this.callback?.('play') } else if (this.audio.played) { /* empty */ } - else { this.audio.play() this.callback?.('play') @@ -221,7 +206,6 @@ export default class AudioPlayer { this.finishStream() return } - if (this.sourceBuffer?.updating) { this.cacheBuffers.push(audioData) } diff --git a/web/app/components/base/audio-gallery/AudioPlayer.tsx b/web/app/components/base/audio-gallery/AudioPlayer.tsx index cbf50ddc13..5a0a753ecf 100644 --- a/web/app/components/base/audio-gallery/AudioPlayer.tsx +++ b/web/app/components/base/audio-gallery/AudioPlayer.tsx @@ -1,7 +1,7 @@ import { t } from 'i18next' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' import { cn } from '@/utils/classnames' @@ -10,7 +10,6 @@ type AudioPlayerProps = { src?: string // Keep backward compatibility srcs?: string[] // Support multiple sources } - const AudioPlayer: React.FC = ({ src, srcs }) => { const [isPlaying, setIsPlaying] = useState(false) const [currentTime, setCurrentTime] = useState(0) @@ -23,43 +22,34 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { const [hoverTime, setHoverTime] = useState(0) const [isAudioAvailable, setIsAudioAvailable] = useState(true) const { theme } = useTheme() - useEffect(() => { const audio = audioRef.current /* v8 ignore next 2 - @preserve */ if (!audio) return - const handleError = () => { setIsAudioAvailable(false) } - const setAudioData = () => { setDuration(audio.duration) } - const setAudioTime = () => { setCurrentTime(audio.currentTime) } - const handleProgress = () => { if (audio.buffered.length > 0) setBufferedTime(audio.buffered.end(audio.buffered.length - 1)) } - const handleEnded = () => { setIsPlaying(false) } - audio.addEventListener('loadedmetadata', setAudioData) audio.addEventListener('timeupdate', setAudioTime) audio.addEventListener('progress', handleProgress) audio.addEventListener('ended', handleEnded) audio.addEventListener('error', handleError) - // Preload audio metadata audio.load() - // Use the first source or src to generate waveform const primarySrc = srcs?.[0] || src if (primarySrc) { @@ -76,17 +66,12 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { } } }, [src, srcs]) - const generateWaveformData = async (audioSrc: string) => { if (!window.AudioContext && !(window as any).webkitAudioContext) { setIsAudioAvailable(false) - Toast.notify({ - type: 'error', - message: 'Web Audio API is not supported in this browser', - }) + toast.error('Web Audio API is not supported in this browser') return null } - const primarySrc = srcs?.[0] || src const url = primarySrc ? new URL(primarySrc) : null const isHttp = url ? (url.protocol === 'http:' || url.protocol === 'https:') : false @@ -94,53 +79,43 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { setIsAudioAvailable(false) return null } - const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() const samples = 70 - try { const response = await fetch(audioSrc, { mode: 'cors' }) if (!response || !response.ok) { setIsAudioAvailable(false) return null } - const arrayBuffer = await response.arrayBuffer() const audioBuffer = await audioContext.decodeAudioData(arrayBuffer) const channelData = audioBuffer.getChannelData(0) const blockSize = Math.floor(channelData.length / samples) const waveformData: number[] = [] - for (let i = 0; i < samples; i++) { let sum = 0 for (let j = 0; j < blockSize; j++) sum += Math.abs(channelData[i * blockSize + j]) - // Apply nonlinear scaling to enhance small amplitudes waveformData.push((sum / blockSize) * 5) } - // Normalized waveform data const maxAmplitude = Math.max(...waveformData) const normalizedWaveform = waveformData.map(amp => amp / maxAmplitude) - setWaveformData(normalizedWaveform) setIsAudioAvailable(true) } catch { const waveform: number[] = [] let prevValue = Math.random() - for (let i = 0; i < samples; i++) { const targetValue = Math.random() const interpolatedValue = prevValue + (targetValue - prevValue) * 0.3 waveform.push(interpolatedValue) prevValue = interpolatedValue } - const maxAmplitude = Math.max(...waveform) const randomWaveform = waveform.map(amp => amp / maxAmplitude) - setWaveformData(randomWaveform) setIsAudioAvailable(true) } @@ -148,7 +123,6 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { await audioContext.close() } } - const togglePlay = useCallback(() => { const audio = audioRef.current if (audio && isAudioAvailable) { @@ -160,99 +134,75 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { setHasStartedPlaying(true) audio.play().catch(error => console.error('Error playing audio:', error)) } - setIsPlaying(!isPlaying) } else { - Toast.notify({ - type: 'error', - message: 'Audio element not found', - }) + toast.error('Audio element not found') setIsAudioAvailable(false) } }, [isAudioAvailable, isPlaying]) - const handleCanvasInteraction = useCallback((e: React.MouseEvent | React.TouchEvent) => { e.preventDefault() - const getClientX = (event: React.MouseEvent | React.TouchEvent): number => { if ('touches' in event) return event.touches[0].clientX return event.clientX } - const updateProgress = (clientX: number) => { const canvas = canvasRef.current const audio = audioRef.current if (!canvas || !audio) return - const rect = canvas.getBoundingClientRect() const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width const newTime = percent * duration - // Removes the buffer check, allowing drag to any location audio.currentTime = newTime setCurrentTime(newTime) - if (!isPlaying) { setIsPlaying(true) audio.play().catch((error) => { - Toast.notify({ - type: 'error', - message: `Error playing audio: ${error}`, - }) + toast.error(`Error playing audio: ${error}`) setIsPlaying(false) }) } } - updateProgress(getClientX(e)) }, [duration, isPlaying]) - const formatTime = (time: number) => { const minutes = Math.floor(time / 60) const seconds = Math.floor(time % 60) return `${minutes}:${seconds.toString().padStart(2, '0')}` } - const drawWaveform = useCallback(() => { const canvas = canvasRef.current /* v8 ignore next 2 - @preserve */ if (!canvas) return - const ctx = canvas.getContext('2d') if (!ctx) return - const width = canvas.width const height = canvas.height const data = waveformData - ctx.clearRect(0, 0, width, height) - const barWidth = width / data.length const playedWidth = (currentTime / duration) * width const cornerRadius = 2 - // Draw waveform bars data.forEach((value, index) => { let color - if (index * barWidth <= playedWidth) color = theme === Theme.light ? '#296DFF' : '#84ABFF' else if ((index * barWidth / width) * duration <= hoverTime) color = theme === Theme.light ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)' else color = theme === Theme.light ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)' - const barHeight = value * height const rectX = index * barWidth const rectY = (height - barHeight) / 2 const rectWidth = barWidth * 0.5 const rectHeight = barHeight - ctx.lineWidth = 1 ctx.fillStyle = color if (ctx.roundRect) { @@ -265,27 +215,22 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { } }) }, [currentTime, duration, hoverTime, theme, waveformData]) - useEffect(() => { drawWaveform() }, [drawWaveform, bufferedTime, hasStartedPlaying]) - const handleMouseMove = useCallback((e: React.MouseEvent | React.TouchEvent) => { const canvas = canvasRef.current const audio = audioRef.current if (!canvas || !audio) return - const clientX = 'touches' in e ? e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX : e.clientX if (clientX === undefined) return - const rect = canvas.getBoundingClientRect() const percent = Math.min(Math.max(0, clientX - rect.left), rect.width) / rect.width const time = percent * duration - // Check if the hovered position is within a buffered range before updating hoverTime for (let i = 0; i < audio.buffered.length; i++) { if (time >= audio.buffered.start(i) && time <= audio.buffered.end(i)) { @@ -294,38 +239,20 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { } } }, [duration]) - return (
- ) } - const placeholder = '' const editAreaClassName = 'focus:outline-none bg-transparent text-sm' - const textAreaContent = (
!readonly && setIsEditing(true)}> {isEditing @@ -134,10 +105,10 @@ const BlockInput: FC = ({ onBlur={() => { blur() setIsEditing(false) - // click confirm also make blur. Then outer value is change. So below code has problem. - // setTimeout(() => { - // handleCancel() - // }, 1000) + // click confirm also make blur. Then outer value is change. So below code has problem. + // setTimeout(() => { + // handleCancel() + // }, 1000) }} />
@@ -145,7 +116,6 @@ const BlockInput: FC = ({ : }
) - return (
{textAreaContent} @@ -159,5 +129,4 @@ const BlockInput: FC = ({
) } - export default React.memo(BlockInput) diff --git a/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx index b004a1bee6..f4c8ef0c45 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx @@ -4,7 +4,7 @@ import type { InstalledApp } from '@/models/explore' import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook, waitFor } from '@testing-library/react' -import { ToastProvider } from '@/app/components/base/toast' +import { ToastHost } from '@/app/components/base/ui/toast' import { AppSourceType, delConversation, @@ -95,7 +95,8 @@ const createQueryClient = () => new QueryClient({ const createWrapper = (queryClient: QueryClient) => { return ({ children }: { children: ReactNode }) => ( - {children} + + {children} ) } diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index e19c57bd83..4bd894d63f 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -1,47 +1,21 @@ import type { ExtraContent } from '../chat/type' -import type { - Callback, - ChatConfig, - ChatItem, - Feedback, -} from '../types' +import type { Callback, ChatConfig, ChatItem, Feedback } from '../types' import type { InstalledApp } from '@/models/explore' -import type { - AppData, - ConversationItem, -} from '@/models/share' +import type { AppData, ConversationItem } from '@/models/share' import type { HumanInputFilledFormData, HumanInputFormData } from '@/types/workflow' import { useLocalStorageState } from 'ahooks' import { noop } from 'es-toolkit/function' import { produce } from 'immer' -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { InputVarType } from '@/app/components/workflow/types' import { useWebAppStore } from '@/context/web-app-context' import { useAppFavicon } from '@/hooks/use-app-favicon' import { changeLanguage } from '@/i18n-config/client' -import { - AppSourceType, - delConversation, - pinConversation, - renameConversation, - unpinConversation, - updateFeedback, -} from '@/service/share' -import { - useInvalidateShareConversations, - useShareChatList, - useShareConversationName, - useShareConversations, -} from '@/service/use-share' +import { AppSourceType, delConversation, pinConversation, renameConversation, unpinConversation, updateFeedback } from '@/service/share' +import { useInvalidateShareConversations, useShareChatList, useShareConversationName, useShareConversations } from '@/service/use-share' import { TransferMethod } from '@/types/app' import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { CONVERSATION_ID_INFO } from '../constants' @@ -94,14 +68,12 @@ function getFormattedChatList(messages: any[]) { }) return newChatList } - export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp const appInfo = useWebAppStore(s => s.appInfo) const appParams = useWebAppStore(s => s.appParams) const appMeta = useWebAppStore(s => s.appMeta) - useAppFavicon({ enable: !installedAppInfo, icon_type: appInfo?.site.icon_type, @@ -109,7 +81,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { icon_background: appInfo?.site.icon_background, icon_url: appInfo?.site.icon_url, }) - const appData = useMemo(() => { if (isInstalledApp) { const { id, app } = installedAppInfo! @@ -130,18 +101,15 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { custom_config: null, } as AppData } - return appInfo }, [isInstalledApp, installedAppInfo, appInfo]) const appId = useMemo(() => appData?.app_id, [appData]) - const [userId, setUserId] = useState() useEffect(() => { getProcessedSystemVariablesFromUrlParams().then(({ user_id }) => { setUserId(user_id) }) }, []) - useEffect(() => { const setLocaleFromProps = async () => { if (appData?.site.default_language) @@ -149,7 +117,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } setLocaleFromProps() }, [appData]) - const [sidebarCollapseState, setSidebarCollapseState] = useState(() => { if (typeof window !== 'undefined') { try { @@ -193,15 +160,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }) } }, [appId, conversationIdInfo, setConversationIdInfo, userId]) - const [newConversationId, setNewConversationId] = useState('') const chatShouldReloadKey = useMemo(() => { if (currentConversationId === newConversationId) return '' - return currentConversationId }, [currentConversationId, newConversationId]) - const { data: appPinnedConversationData } = useShareConversations({ appSourceType, appId, @@ -212,10 +176,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { refetchOnWindowFocus: false, refetchOnReconnect: false, }) - const { - data: appConversationData, - isLoading: appConversationDataLoading, - } = useShareConversations({ + const { data: appConversationData, isLoading: appConversationDataLoading } = useShareConversations({ appSourceType, appId, pinned: false, @@ -225,10 +186,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { refetchOnWindowFocus: false, refetchOnReconnect: false, }) - const { - data: appChatListData, - isLoading: appChatListDataLoading, - } = useShareChatList({ + const { data: appChatListData, isLoading: appChatListDataLoading } = useShareChatList({ conversationId: chatShouldReloadKey, appSourceType, appId, @@ -238,18 +196,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { refetchOnReconnect: false, }) const invalidateShareConversations = useInvalidateShareConversations() - const [clearChatList, setClearChatList] = useState(false) const [isResponding, setIsResponding] = useState(false) - const appPrevChatTree = useMemo( - () => (currentConversationId && appChatListData?.data.length) - ? buildChatItemTree(getFormattedChatList(appChatListData.data)) - : [], - [appChatListData, currentConversationId], - ) - + const appPrevChatTree = useMemo(() => (currentConversationId && appChatListData?.data.length) + ? buildChatItemTree(getFormattedChatList(appChatListData.data)) + : [], [appChatListData, currentConversationId]) const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false) - const pinnedConversationList = useMemo(() => { return appPinnedConversationData?.data || [] }, [appPinnedConversationData]) @@ -268,7 +220,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { let value = initInputs[item.paragraph.variable] if (value && item.paragraph.max_length && value.length > item.paragraph.max_length) value = value.slice(0, item.paragraph.max_length) - return { ...item.paragraph, default: value || item.default || item.paragraph.default, @@ -283,7 +234,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { type: 'number', } } - if (item.checkbox) { const preset = initInputs[item.checkbox.variable] === true return { @@ -292,7 +242,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { type: 'checkbox', } } - if (item.select) { const isInputInOptions = item.select.options.includes(initInputs[item.select.variable]) return { @@ -301,32 +250,27 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { type: 'select', } } - if (item['file-list']) { return { ...item['file-list'], type: 'file-list', } } - if (item.file) { return { ...item.file, type: 'file', } } - if (item.json_object) { return { ...item.json_object, type: 'json_object', } } - let value = initInputs[item['text-input'].variable] if (value && item['text-input'].max_length && value.length > item['text-input'].max_length) value = value.slice(0, item['text-input'].max_length) - return { ...item['text-input'], default: value || item.default || item['text-input'].default, @@ -334,11 +278,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } }) }, [initInputs, appParams]) - const allInputsHidden = useMemo(() => { return inputsForms.length > 0 && inputsForms.every(item => item.hide === true) }, [inputsForms]) - useEffect(() => { // init inputs from url params (async () => { @@ -348,16 +290,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { setInitUserVariables(userVariables) })() }, []) - useEffect(() => { const conversationInputs: Record = {} - inputsForms.forEach((item: any) => { conversationInputs[item.variable] = item.default || null }) handleNewConversationInputsChange(conversationInputs) }, [handleNewConversationInputsChange, inputsForms]) - const { data: newConversation } = useShareConversationName({ conversationId: newConversationId, appSourceType, @@ -373,7 +312,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [appConversationData, appConversationDataLoading]) const conversationList = useMemo(() => { const data = originConversationList.slice() - if (showNewConversationItemInList && data[0]?.id !== '') { data.unshift({ id: '', @@ -384,12 +322,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } return data }, [originConversationList, showNewConversationItemInList, t]) - useEffect(() => { if (newConversation) { setOriginConversationList(produce((draft) => { const index = draft.findIndex(item => item.id === newConversation.id) - if (index > -1) draft[index] = newConversation else @@ -397,16 +333,12 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { })) } }, [newConversation]) - const currentConversationItem = useMemo(() => { let conversationItem = conversationList.find(item => item.id === currentConversationId) - if (!conversationItem && pinnedConversationList.length) conversationItem = pinnedConversationList.find(item => item.id === currentConversationId) - return conversationItem }, [conversationList, currentConversationId, pinnedConversationList]) - const currentConversationLatestInputs = useMemo(() => { if (!currentConversationId || !appChatListData?.data.length) return newConversationInputsRef.current || {} @@ -417,12 +349,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { if (currentConversationItem) setCurrentConversationInputs(currentConversationLatestInputs || {}) }, [currentConversationItem, currentConversationLatestInputs]) - - const { notify } = useToastContext() const checkInputsRequired = useCallback((silent?: boolean) => { if (allInputsHidden) return true - let hasEmptyInput = '' let fileIsUploading = false const requiredVars = inputsForms.filter(({ required, type }) => required && type !== InputVarType.checkbox) @@ -430,13 +359,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { requiredVars.forEach(({ variable, label, type }) => { if (hasEmptyInput) return - if (fileIsUploading) return - if (!newConversationInputsRef.current[variable] && !silent) hasEmptyInput = label as string - if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) { const files = newConversationInputsRef.current[variable] if (Array.isArray(files)) @@ -446,26 +372,25 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { } }) } - if (hasEmptyInput) { - notify({ type: 'error', message: t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput }) }) + toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: hasEmptyInput })) return false } - if (fileIsUploading) { - notify({ type: 'info', message: t('errorMessage.waitForFileUpload', { ns: 'appDebug' }) }) + toast.info(t('errorMessage.waitForFileUpload', { ns: 'appDebug' })) return } - return true - }, [inputsForms, notify, t, allInputsHidden]) + }, [inputsForms, t, allInputsHidden]) const handleStartChat = useCallback((callback: any) => { if (checkInputsRequired()) { setShowNewConversationItemInList(true) callback?.() } }, [setShowNewConversationItemInList, checkInputsRequired]) - const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: noop }) + const currentChatInstanceRef = useRef<{ + handleStop: () => void + }>({ handleStop: noop }) const handleChangeConversation = useCallback((conversationId: string) => { currentChatInstanceRef.current.handleStop() setNewConversationId('') @@ -487,76 +412,48 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const handleUpdateConversationList = useCallback(() => { invalidateShareConversations() }, [invalidateShareConversations]) - const handlePinConversation = useCallback(async (conversationId: string) => { await pinConversation(appSourceType, appId, conversationId) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) + toast.success(t('api.success', { ns: 'common' })) handleUpdateConversationList() - }, [appSourceType, appId, notify, t, handleUpdateConversationList]) - + }, [appSourceType, appId, t, handleUpdateConversationList]) const handleUnpinConversation = useCallback(async (conversationId: string) => { await unpinConversation(appSourceType, appId, conversationId) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) + toast.success(t('api.success', { ns: 'common' })) handleUpdateConversationList() - }, [appSourceType, appId, notify, t, handleUpdateConversationList]) - + }, [appSourceType, appId, t, handleUpdateConversationList]) const [conversationDeleting, setConversationDeleting] = useState(false) - const handleDeleteConversation = useCallback(async ( - conversationId: string, - { - onSuccess, - }: Callback, - ) => { + const handleDeleteConversation = useCallback(async (conversationId: string, { onSuccess }: Callback) => { if (conversationDeleting) return - try { setConversationDeleting(true) await delConversation(appSourceType, appId, conversationId) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) + toast.success(t('api.success', { ns: 'common' })) onSuccess() } finally { setConversationDeleting(false) } - if (conversationId === currentConversationId) handleNewConversation() - handleUpdateConversationList() - }, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting]) - + }, [isInstalledApp, appId, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting]) const [conversationRenaming, setConversationRenaming] = useState(false) - const handleRenameConversation = useCallback(async ( - conversationId: string, - newName: string, - { - onSuccess, - }: Callback, - ) => { + const handleRenameConversation = useCallback(async (conversationId: string, newName: string, { onSuccess }: Callback) => { if (conversationRenaming) return - if (!newName.trim()) { - notify({ - type: 'error', - message: t('chat.conversationNameCanNotEmpty', { ns: 'common' }), - }) + toast.error(t('chat.conversationNameCanNotEmpty', { ns: 'common' })) return } - setConversationRenaming(true) try { await renameConversation(appSourceType, appId, conversationId, newName) - - notify({ - type: 'success', - message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }), - }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) setOriginConversationList(produce((draft) => { const index = originConversationList.findIndex(item => item.id === conversationId) const item = draft[index] - draft[index] = { ...item, name: newName, @@ -567,20 +464,17 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { finally { setConversationRenaming(false) } - }, [isInstalledApp, appId, notify, t, conversationRenaming, originConversationList]) - + }, [isInstalledApp, appId, t, conversationRenaming, originConversationList]) const handleNewConversationCompleted = useCallback((newConversationId: string) => { setNewConversationId(newConversationId) handleConversationIdInfoChange(newConversationId) setShowNewConversationItemInList(false) invalidateShareConversations() }, [handleConversationIdInfoChange, invalidateShareConversations]) - const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => { await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating, content: feedback.content } }, appSourceType, appId) - notify({ type: 'success', message: t('api.success', { ns: 'common' }) }) - }, [appSourceType, appId, t, notify]) - + toast.success(t('api.success', { ns: 'common' })) + }, [appSourceType, appId, t]) return { isInstalledApp, appId, diff --git a/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx index 6afbc26582..b2bdfc1f4c 100644 --- a/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/check-input-forms-hooks.spec.tsx @@ -5,8 +5,8 @@ import { TransferMethod } from '@/types/app' import { useCheckInputsForms } from '../check-input-forms-hooks' const mockNotify = vi.fn() -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ notify: mockNotify }), +vi.mock('@/app/components/base/ui/toast', () => ({ + })) describe('useCheckInputsForms', () => { diff --git a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx index 92fa9ea42e..1f66230134 100644 --- a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx @@ -20,8 +20,8 @@ vi.mock('@/app/components/base/audio-btn/audio.player.manager', () => ({ }, })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ notify: vi.fn() }), +vi.mock('@/app/components/base/ui/toast', () => ({ + })) vi.mock('@/hooks/use-timestamp', () => ({ diff --git a/web/app/components/base/chat/chat/__tests__/question.spec.tsx b/web/app/components/base/chat/chat/__tests__/question.spec.tsx index e9392adb8a..9d49be3a15 100644 --- a/web/app/components/base/chat/chat/__tests__/question.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/question.spec.tsx @@ -5,7 +5,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import copy from 'copy-to-clipboard' import * as React from 'react' -import Toast from '../../../toast' +import { toast } from '@/app/components/base/ui/toast' import { ThemeBuilder } from '../../embedded-chatbot/theme/theme-context' import { ChatContextProvider } from '../context-provider' import Question from '../question' @@ -179,7 +179,7 @@ describe('Question component', () => { it('should call copy-to-clipboard and show a toast when copy action is clicked', async () => { const user = userEvent.setup() - const toastSpy = vi.spyOn(Toast, 'notify') + const toastSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success') renderWithProvider(makeItem()) diff --git a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx index c17dd5ad92..c2e840afb7 100644 --- a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx +++ b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx @@ -29,7 +29,7 @@ const { vi.mock('copy-to-clipboard', () => ({ default: vi.fn() })) -vi.mock('@/app/components/base/toast', () => ({ +vi.mock('@/app/components/base/ui/toast', () => ({ default: { notify: vi.fn() }, })) diff --git a/web/app/components/base/chat/chat/answer/operation.tsx b/web/app/components/base/chat/chat/answer/operation.tsx index a1bc70512b..826ce7031d 100644 --- a/web/app/components/base/chat/chat/answer/operation.tsx +++ b/web/app/components/base/chat/chat/answer/operation.tsx @@ -1,14 +1,7 @@ import type { FC } from 'react' -import type { - ChatItem, - Feedback, -} from '../../types' +import type { ChatItem, Feedback } from '../../types' import copy from 'copy-to-clipboard' -import { - memo, - useMemo, - useState, -} from 'react' +import { memo, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' @@ -17,8 +10,8 @@ import AnnotationCtrlButton from '@/app/components/base/features/new-feature-pan import Modal from '@/app/components/base/modal/modal' import NewAudioButton from '@/app/components/base/new-audio-button' import Textarea from '@/app/components/base/textarea' -import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { cn } from '@/utils/classnames' import { useChatContext } from '../context' @@ -32,14 +25,11 @@ type OperationProps = { hasWorkflowProcess: boolean noChatInput?: boolean } - const stringifyCopyValue = (value: unknown) => { if (typeof value === 'string') return value - if (value === null || typeof value === 'undefined') return '' - try { return JSON.stringify(value, null, 2) } @@ -47,196 +37,132 @@ const stringifyCopyValue = (value: unknown) => { return String(value) } } - const buildCopyContentFromLLMGenerationItems = (llmGenerationItems?: ChatItem['llmGenerationItems']) => { if (!llmGenerationItems?.length) return '' - const hasStructuredItems = llmGenerationItems.some(item => item.type !== 'text') if (!hasStructuredItems) return '' - return llmGenerationItems .map((item) => { if (item.type === 'text') return item.text || '' - if (item.type === 'thought') return item.thoughtOutput ? `[THOUGHT]\n${item.thoughtOutput}` : '' - if (item.type === 'tool') { const sections = [ `[TOOL] ${item.toolName || ''}`.trim(), ] - if (item.toolArguments) sections.push(`INPUT:\n${stringifyCopyValue(item.toolArguments)}`) if (typeof item.toolOutput !== 'undefined') sections.push(`OUTPUT:\n${stringifyCopyValue(item.toolOutput)}`) if (item.toolError) sections.push(`ERROR:\n${item.toolError}`) - return sections.join('\n') } - if (item.type === 'model') { const sections = [ `[MODEL] ${item.modelName || ''}`.trim(), ] - if (typeof item.modelOutput !== 'undefined') sections.push(`OUTPUT:\n${stringifyCopyValue(item.modelOutput)}`) - return sections.join('\n') } - return '' }) .filter(Boolean) .join('\n\n') } - const buildCopyContentFromAgentThoughts = (agentThoughts?: ChatItem['agent_thoughts']) => { if (!agentThoughts?.length) return '' - return agentThoughts .map((thought) => { const sections = [ `[AGENT] ${thought.tool || ''}`.trim(), ] - if (thought.thought) sections.push(`THOUGHT:\n${thought.thought}`) if (thought.tool_input) sections.push(`INPUT:\n${thought.tool_input}`) if (thought.observation) sections.push(`OUTPUT:\n${thought.observation}`) - return sections.join('\n') }) .join('\n\n') } - -const Operation: FC = ({ - item, - question, - index, - showPromptLog, - maxSize, - contentWidth, - hasWorkflowProcess, - noChatInput, -}) => { +const Operation: FC = ({ item, question, index, showPromptLog, maxSize, contentWidth, hasWorkflowProcess, noChatInput }) => { const { t } = useTranslation() - const { - config, - onAnnotationAdded, - onAnnotationEdited, - onAnnotationRemoved, - onFeedback, - onRegenerate, - } = useChatContext() + const { config, onAnnotationAdded, onAnnotationEdited, onAnnotationRemoved, onFeedback, onRegenerate } = useChatContext() const [isShowReplyModal, setIsShowReplyModal] = useState(false) const [isShowFeedbackModal, setIsShowFeedbackModal] = useState(false) const [feedbackContent, setFeedbackContent] = useState('') - const { - id, - isOpeningStatement, - content: messageContent, - annotation, - feedback, - adminFeedback, - agent_thoughts, - humanInputFormDataList, - } = item + const { id, isOpeningStatement, content: messageContent, annotation, feedback, adminFeedback, agent_thoughts, humanInputFormDataList } = item const [userLocalFeedback, setUserLocalFeedback] = useState(feedback) const [adminLocalFeedback, setAdminLocalFeedback] = useState(adminFeedback) const [feedbackTarget, setFeedbackTarget] = useState<'user' | 'admin'>('user') - // Separate feedback types for display const userFeedback = feedback - const content = useMemo(() => { if (agent_thoughts?.length) return agent_thoughts.reduce((acc, cur) => acc + cur.thought, '') - return messageContent }, [agent_thoughts, messageContent]) - const copyContent = useMemo(() => { const llmGenerationCopyContent = buildCopyContentFromLLMGenerationItems(item.llmGenerationItems) if (llmGenerationCopyContent) return llmGenerationCopyContent - const agentThoughtCopyContent = buildCopyContentFromAgentThoughts(agent_thoughts) if (agentThoughtCopyContent) return agentThoughtCopyContent - return messageContent }, [item.llmGenerationItems, agent_thoughts, messageContent]) - const displayUserFeedback = userLocalFeedback ?? userFeedback - const hasUserFeedback = !!displayUserFeedback?.rating const hasAdminFeedback = !!adminLocalFeedback?.rating - const shouldShowUserFeedbackBar = !isOpeningStatement && config?.supportFeedback && !!onFeedback && !config?.supportAnnotation const shouldShowAdminFeedbackBar = !isOpeningStatement && config?.supportFeedback && !!onFeedback && !!config?.supportAnnotation - const userFeedbackLabel = t('table.header.userRate', { ns: 'appLog' }) || 'User feedback' const adminFeedbackLabel = t('table.header.adminRate', { ns: 'appLog' }) || 'Admin feedback' const feedbackTooltipClassName = 'max-w-[260px]' - const buildFeedbackTooltip = (feedbackData?: Feedback | null, label = userFeedbackLabel) => { if (!feedbackData?.rating) return label - const ratingLabel = feedbackData.rating === 'like' ? (t('detail.operation.like', { ns: 'appLog' }) || 'like') : (t('detail.operation.dislike', { ns: 'appLog' }) || 'dislike') const feedbackText = feedbackData.content?.trim() - if (feedbackText) return `${label}: ${ratingLabel} - ${feedbackText}` - return `${label}: ${ratingLabel}` } - const handleFeedback = async (rating: 'like' | 'dislike' | null, content?: string, target: 'user' | 'admin' = 'user') => { if (!config?.supportFeedback || !onFeedback) return - await onFeedback?.(id, { rating, content }) - const nextFeedback = rating === null ? { rating: null } : { rating, content } - if (target === 'admin') setAdminLocalFeedback(nextFeedback) else setUserLocalFeedback(nextFeedback) } - const handleLikeClick = (target: 'user' | 'admin') => { handleFeedback('like', undefined, target) } - const handleDislikeClick = (target: 'user' | 'admin') => { setFeedbackTarget(target) setIsShowFeedbackModal(true) } - const handleFeedbackSubmit = async () => { await handleFeedback('dislike', feedbackContent, feedbackTarget) setFeedbackContent('') setIsShowFeedbackModal(false) } - const handleFeedbackCancel = () => { setFeedbackContent('') setIsShowFeedbackModal(false) } - const operationWidth = useMemo(() => { let width = 0 if (!isOpeningStatement) @@ -251,40 +177,18 @@ const Operation: FC = ({ width += hasUserFeedback ? 28 + 8 : 60 + 8 if (shouldShowAdminFeedbackBar) width += (hasAdminFeedback ? 28 : 60) + 8 + (hasUserFeedback ? 28 : 0) - return width }, [config?.annotation_reply?.enabled, config?.supportAnnotation, config?.text_to_speech?.enabled, hasAdminFeedback, hasUserFeedback, isOpeningStatement, shouldShowAdminFeedbackBar, shouldShowUserFeedbackBar, showPromptLog]) - const positionRight = useMemo(() => operationWidth < maxSize, [operationWidth, maxSize]) - return ( <> -
+
{shouldShowUserFeedbackBar && !humanInputFormDataList?.length && ( -
+
{hasUserFeedback ? ( - - handleFeedback(null, undefined, 'user')} - > + + handleFeedback(null, undefined, 'user')}> {displayUserFeedback?.rating === 'like' ?
:
} @@ -293,16 +197,10 @@ const Operation: FC = ({ ) : ( <> - handleLikeClick('user')} - > + handleLikeClick('user')}>
- handleDislikeClick('user')} - > + handleDislikeClick('user')}>
@@ -310,17 +208,10 @@ const Operation: FC = ({
)} {shouldShowAdminFeedbackBar && !humanInputFormDataList?.length && ( -
+
{/* User Feedback Display */} {displayUserFeedback?.rating && ( - + {displayUserFeedback.rating === 'like' ? ( @@ -339,14 +230,8 @@ const Operation: FC = ({ {displayUserFeedback?.rating &&
} {hasAdminFeedback ? ( - - handleFeedback(null, undefined, 'admin')} - > + + handleFeedback(null, undefined, 'admin')}> {adminLocalFeedback?.rating === 'like' ?
:
} @@ -355,25 +240,13 @@ const Operation: FC = ({ ) : ( <> - - handleLikeClick('admin')} - > + + handleLikeClick('admin')}>
- - handleDislikeClick('admin')} - > + + handleDislikeClick('admin')}>
@@ -388,18 +261,12 @@ const Operation: FC = ({ )} {!isOpeningStatement && (
- {(config?.text_to_speech?.enabled && !humanInputFormDataList?.length) && ( - - )} + {(config?.text_to_speech?.enabled && !humanInputFormDataList?.length) && ()} {!humanInputFormDataList?.length && ( { copy(copyContent) - Toast.notify({ type: 'success', message: t('actionMsg.copySuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.copySuccessfully', { ns: 'common' })) }} data-testid="copy-btn" > @@ -411,55 +278,19 @@ const Operation: FC = ({
)} - {config?.supportAnnotation && config.annotation_reply?.enabled && !humanInputFormDataList?.length && ( - onAnnotationAdded?.(id, authorName, question, content, index)} - onEdit={() => setIsShowReplyModal(true)} - /> - )} + {config?.supportAnnotation && config.annotation_reply?.enabled && !humanInputFormDataList?.length && ( onAnnotationAdded?.(id, authorName, question, content, index)} onEdit={() => setIsShowReplyModal(true)} />)}
)}
- setIsShowReplyModal(false)} - query={question} - answer={content} - onEdited={(editedQuery, editedAnswer) => onAnnotationEdited?.(editedQuery, editedAnswer, index)} - onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded?.(annotationId, authorName, editedQuery, editedAnswer, index)} - appId={config?.appId || ''} - messageId={id} - annotationId={annotation?.id || ''} - createdAt={annotation?.created_at} - onRemove={() => onAnnotationRemoved?.(index)} - /> + setIsShowReplyModal(false)} query={question} answer={content} onEdited={(editedQuery, editedAnswer) => onAnnotationEdited?.(editedQuery, editedAnswer, index)} onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded?.(annotationId, authorName, editedQuery, editedAnswer, index)} appId={config?.appId || ''} messageId={id} annotationId={annotation?.id || ''} createdAt={annotation?.created_at} onRemove={() => onAnnotationRemoved?.(index)} /> {isShowFeedbackModal && ( - +
-