diff --git a/api/pyproject.toml b/api/pyproject.toml index 9ff4c3f5de..1cf7d719ea 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "numpy~=1.26.4", "openpyxl~=3.1.5", "opik~=1.8.72", - "litellm==1.77.1", # Pinned to avoid madoka dependency issue + "litellm==1.77.1", # Pinned to avoid madoka dependency issue "opentelemetry-api==1.27.0", "opentelemetry-distro==0.48b0", "opentelemetry-exporter-otlp==1.27.0", @@ -79,7 +79,6 @@ dependencies = [ "tiktoken~=0.9.0", "transformers~=4.56.1", "unstructured[docx,epub,md,ppt,pptx]~=0.16.1", - "weave~=0.51.0", "yarl~=1.18.3", "webvtt-py~=0.5.1", "sseclient-py~=1.8.0", @@ -90,6 +89,7 @@ dependencies = [ "croniter>=6.0.0", "weaviate-client==4.17.0", "apscheduler>=3.11.0", + "weave>=0.52.16", ] # Before adding new dependency, consider place it in # alphabet order (a-z) and suitable group. diff --git a/api/uv.lock b/api/uv.lock index 439a0566e8..6300adae61 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1588,7 +1588,7 @@ requires-dist = [ { name = "tiktoken", specifier = "~=0.9.0" }, { name = "transformers", specifier = "~=4.56.1" }, { name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" }, - { name = "weave", specifier = "~=0.51.0" }, + { name = "weave", specifier = ">=0.52.16" }, { name = "weaviate-client", specifier = "==4.17.0" }, { name = "webvtt-py", specifier = "~=0.5.1" }, { name = "yarl", specifier = "~=1.18.3" }, @@ -3538,15 +3538,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/e1/45373c06781340c7b74fe9b88b85278ac05321889a307eaa5be079a997d4/mysql_connector_python-9.5.0-py2.py3-none-any.whl", hash = "sha256:ace137b88eb6fdafa1e5b2e03ac76ce1b8b1844b3a4af1192a02ae7c1a45bdee", size = 479047, upload-time = "2025-10-22T09:02:27.809Z" }, ] -[[package]] -name = "nest-asyncio" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, -] - [[package]] name = "networkx" version = "3.5" @@ -6906,7 +6897,7 @@ wheels = [ [[package]] name = "weave" -version = "0.51.59" +version = "0.52.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -6914,18 +6905,17 @@ dependencies = [ { name = "eval-type-backport" }, { name = "gql", extra = ["aiohttp", "requests"] }, { name = "jsonschema" }, - { name = "nest-asyncio" }, { name = "packaging" }, { name = "polyfile-weave" }, { name = "pydantic" }, - { name = "rich" }, { name = "sentry-sdk" }, { name = "tenacity" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "wandb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/53/1b0350a64837df3e29eda6149a542f3a51e706122086f82547153820e982/weave-0.51.59.tar.gz", hash = "sha256:fad34c0478f3470401274cba8fa2bfd45d14a187db0a5724bd507e356761b349", size = 480572, upload-time = "2025-07-25T22:05:07.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/30/b795b5a857e8a908e68f3ed969587bb2bc63527ef2260f72ac1a6fd983e8/weave-0.52.16.tar.gz", hash = "sha256:7bb8fdce0393007e9c40fb1769d0606bfe55401c4cd13146457ccac4b49c695d", size = 607024, upload-time = "2025-11-07T19:45:30.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/bc/fa5ffb887a1ee28109b29c62416c9e0f41da8e75e6871671208b3d42b392/weave-0.51.59-py3-none-any.whl", hash = "sha256:2238578574ecdf6285efdf028c78987769720242ac75b7b84b1dbc59060468ce", size = 612468, upload-time = "2025-07-25T22:05:05.088Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/a54513796605dfaef2c3c23c2733bcb4b24866a623635c057b2ffdb74052/weave-0.52.16-py3-none-any.whl", hash = "sha256:85985b8cf233032c6d915dfac95b3bcccb1304444d99a6b4a61f3666b58146ce", size = 764366, upload-time = "2025-11-07T19:45:28.878Z" }, ] [[package]] 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 7e592729a5..fb431c5ac8 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import AppCard from '@/app/components/app/overview/app-card' @@ -24,6 +24,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { useAppWorkflow } from '@/service/use-workflow' import type { BlockEnum } from '@/app/components/workflow/types' import { isTriggerNode } from '@/app/components/workflow/types' +import { useDocLink } from '@/context/i18n' export type ICardViewProps = { appId: string @@ -33,22 +34,56 @@ export type ICardViewProps = { const CardView: FC = ({ appId, isInPanel, className }) => { const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useContext(ToastContext) const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) + const isWorkflowApp = appDetail?.mode === AppModeEnum.WORKFLOW const showMCPCard = isInPanel - const showTriggerCard = isInPanel && appDetail?.mode === AppModeEnum.WORKFLOW - const { data: currentWorkflow } = useAppWorkflow(appDetail?.mode === AppModeEnum.WORKFLOW ? appDetail.id : '') - const hasTriggerNode = useMemo(() => { - if (appDetail?.mode !== AppModeEnum.WORKFLOW) + const showTriggerCard = isInPanel && isWorkflowApp + const { data: currentWorkflow } = useAppWorkflow(isWorkflowApp ? appDetail.id : '') + const hasTriggerNode = useMemo(() => { + if (!isWorkflowApp) return false - const nodes = currentWorkflow?.graph?.nodes || [] + if (!currentWorkflow) + return null + const nodes = currentWorkflow.graph?.nodes || [] return nodes.some((node) => { const nodeType = node.data?.type as BlockEnum | undefined return !!nodeType && isTriggerNode(nodeType) }) - }, [appDetail?.mode, currentWorkflow]) + }, [isWorkflowApp, currentWorkflow]) + const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false + const disableAppCards = !shouldRenderAppCards + + const triggerDocUrl = docLink('/guides/workflow/node/start') + const buildTriggerModeMessage = useCallback((featureName: string) => ( +
+
+ {t('appOverview.overview.disableTooltip.triggerMode', { feature: featureName })} +
+
{ + event.stopPropagation() + window.open(triggerDocUrl, '_blank') + }} + > + {t('appOverview.overview.appInfo.enableTooltip.learnMore')} +
+
+ ), [t, triggerDocUrl]) + + const disableWebAppTooltip = disableAppCards + ? buildTriggerModeMessage(t('appOverview.overview.appInfo.title')) + : null + const disableApiTooltip = disableAppCards + ? buildTriggerModeMessage(t('appOverview.overview.apiInfo.title')) + : null + const disableMcpTooltip = disableAppCards + ? buildTriggerModeMessage(t('tools.mcp.server.title')) + : null const updateAppDetail = async () => { try { @@ -120,39 +155,48 @@ const CardView: FC = ({ appId, isInPanel, className }) => { if (!appDetail) return - return ( -
- { - !hasTriggerNode && ( - <> - - - {showMCPCard && ( - - )} - - ) - } - {showTriggerCard && ( - + + + {showMCPCard && ( + )} + + ) + + const triggerCardNode = showTriggerCard ? ( + + ) : null + + return ( +
+ {disableAppCards && triggerCardNode} + {appCards} + {!disableAppCards && triggerCardNode}
) } diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 5de86be7b9..54cc345d2e 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -42,6 +42,7 @@ import { getProcessedFilesFromResponse } from '@/app/components/base/file-upload import cn from '@/utils/classnames' import { noop } from 'lodash-es' import PromptLogModal from '../../base/prompt-log-modal' +import { WorkflowContextProvider } from '@/app/components/workflow/context' type AppStoreState = ReturnType type ConversationListItem = ChatConversationGeneralDetail | CompletionConversationGeneralDetail @@ -779,15 +780,17 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { }
{showMessageLogModal && ( - { - setCurrentLogItem() - setShowMessageLogModal(false) - }} - defaultTab={currentLogModalActiveTab} - /> + + { + setCurrentLogItem() + setShowMessageLogModal(false) + }} + defaultTab={currentLogModalActiveTab} + /> + )} {!isChatMode && showPromptLogModal && ( Promise onSaveSiteConfig?: (params: ConfigParams) => Promise onGenerateCode?: () => Promise @@ -61,6 +63,8 @@ function AppCard({ isInPanel, cardType = 'webapp', customBgColor, + triggerModeDisabled = false, + triggerModeMessage = '', onChangeStatus, onSaveSiteConfig, onGenerateCode, @@ -111,7 +115,7 @@ function AppCard({ const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start) const missingStartNode = isWorkflowApp && !hasStartNode const hasInsufficientPermissions = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager - const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode + const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled const runningStatus = (appUnpublished || missingStartNode) ? false : (isApp ? appInfo.enable_site : appInfo.enable_api) const isMinimalState = appUnpublished || missingStartNode const { app_base_url, access_token } = appInfo.site ?? {} @@ -189,7 +193,20 @@ function AppCard({ className={ `${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${isMinimalState ? 'h-12' : ''}`} > -
+
+ {triggerModeDisabled && ( + triggerModeMessage + ? ( + + + + ) + : + )}
-
- {t('appOverview.overview.appInfo.enableTooltip.description')} -
-
window.open(docLink('/guides/workflow/node/user-input'), '_blank')} - > - {t('appOverview.overview.appInfo.enableTooltip.learnMore')} -
- + toggleDisabled ? ( + triggerModeDisabled && triggerModeMessage + ? triggerModeMessage + : (appUnpublished || missingStartNode) ? ( + <> +
+ {t('appOverview.overview.appInfo.enableTooltip.description')} +
+
window.open(docLink('/guides/workflow/node/user-input'), '_blank')} + > + {t('appOverview.overview.appInfo.enableTooltip.learnMore')} +
+ + ) + : '' ) : '' } position="right" @@ -329,9 +351,11 @@ function AppCard({ {!isApp && } {OPERATIONS_MAP[cardType].map((op) => { const disabled - = op.opName === t('appOverview.overview.appInfo.settings.entry') - ? false - : !runningStatus + = triggerModeDisabled + ? true + : op.opName === t('appOverview.overview.appInfo.settings.entry') + ? false + : !runningStatus return (
) diff --git a/web/app/components/base/app-icon/style.module.css b/web/app/components/base/app-icon/style.module.css deleted file mode 100644 index 4ee84fb444..0000000000 --- a/web/app/components/base/app-icon/style.module.css +++ /dev/null @@ -1,23 +0,0 @@ -.appIcon { - @apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0; -} - -.appIcon.large { - @apply w-10 h-10; -} - -.appIcon.small { - @apply w-8 h-8; -} - -.appIcon.tiny { - @apply w-6 h-6 text-base; -} - -.appIcon.xs { - @apply w-5 h-5 text-base; -} - -.appIcon.rounded { - @apply rounded-full; -} diff --git a/web/app/components/base/icons/icon-gallery.stories.tsx b/web/app/components/base/icons/icon-gallery.stories.tsx new file mode 100644 index 0000000000..7da71b3b0b --- /dev/null +++ b/web/app/components/base/icons/icon-gallery.stories.tsx @@ -0,0 +1,258 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import React from 'react' + +declare const require: any + +type IconComponent = React.ComponentType> + +type IconEntry = { + name: string + category: string + path: string + Component: IconComponent +} + +const iconContext = require.context('./src', true, /\.tsx$/) + +const iconEntries: IconEntry[] = iconContext + .keys() + .filter((key: string) => !key.endsWith('.stories.tsx') && !key.endsWith('.spec.tsx')) + .map((key: string) => { + const mod = iconContext(key) + const Component = mod.default as IconComponent | undefined + if (!Component) + return null + + const relativePath = key.replace(/^\.\//, '') + const path = `app/components/base/icons/src/${relativePath}` + const parts = relativePath.split('/') + const fileName = parts.pop() || '' + const category = parts.length ? parts.join('/') : '(root)' + const name = Component.displayName || fileName.replace(/\.tsx$/, '') + + return { + name, + category, + path, + Component, + } + }) + .filter(Boolean) as IconEntry[] + +const sortedEntries = [...iconEntries].sort((a, b) => { + if (a.category === b.category) + return a.name.localeCompare(b.name) + return a.category.localeCompare(b.category) +}) + +const filterEntries = (entries: IconEntry[], query: string) => { + const normalized = query.trim().toLowerCase() + if (!normalized) + return entries + + return entries.filter(entry => + entry.name.toLowerCase().includes(normalized) + || entry.path.toLowerCase().includes(normalized) + || entry.category.toLowerCase().includes(normalized), + ) +} + +const groupByCategory = (entries: IconEntry[]) => entries.reduce((acc, entry) => { + if (!acc[entry.category]) + acc[entry.category] = [] + + acc[entry.category].push(entry) + return acc +}, {} as Record) + +const containerStyle: React.CSSProperties = { + padding: 24, + display: 'flex', + flexDirection: 'column', + gap: 24, +} + +const headerStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 8, +} + +const controlsStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + gap: 12, + flexWrap: 'wrap', +} + +const searchInputStyle: React.CSSProperties = { + padding: '8px 12px', + minWidth: 280, + borderRadius: 6, + border: '1px solid #d0d0d5', +} + +const toggleButtonStyle: React.CSSProperties = { + padding: '8px 12px', + borderRadius: 6, + border: '1px solid #d0d0d5', + background: '#fff', + cursor: 'pointer', +} + +const emptyTextStyle: React.CSSProperties = { color: '#5f5f66' } + +const sectionStyle: React.CSSProperties = { + display: 'flex', + flexDirection: 'column', + gap: 12, +} + +const gridStyle: React.CSSProperties = { + display: 'grid', + gap: 12, + gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', +} + +const cardStyle: React.CSSProperties = { + border: '1px solid #e1e1e8', + borderRadius: 8, + padding: 12, + display: 'flex', + flexDirection: 'column', + gap: 8, + minHeight: 140, +} + +const previewBaseStyle: React.CSSProperties = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: 48, + borderRadius: 6, +} + +const nameButtonBaseStyle: React.CSSProperties = { + display: 'inline-flex', + padding: 0, + border: 'none', + background: 'transparent', + font: 'inherit', + cursor: 'pointer', + textAlign: 'left', + fontWeight: 600, +} + +const PREVIEW_SIZE = 40 + +const IconGalleryStory = () => { + const [query, setQuery] = React.useState('') + const [copiedPath, setCopiedPath] = React.useState(null) + const [previewTheme, setPreviewTheme] = React.useState<'light' | 'dark'>('light') + + const filtered = React.useMemo(() => filterEntries(sortedEntries, query), [query]) + + const grouped = React.useMemo(() => groupByCategory(filtered), [filtered]) + + const categoryOrder = React.useMemo( + () => Object.keys(grouped).sort((a, b) => a.localeCompare(b)), + [grouped], + ) + + React.useEffect(() => { + if (!copiedPath) + return undefined + + const timerId = window.setTimeout(() => { + setCopiedPath(null) + }, 1200) + + return () => window.clearTimeout(timerId) + }, [copiedPath]) + + const handleCopy = React.useCallback((text: string) => { + navigator.clipboard?.writeText(text) + .then(() => { + setCopiedPath(text) + }) + .catch((err) => { + console.error('Failed to copy icon path:', err) + }) + }, []) + + return ( +
+
+

Icon Gallery

+

+ Browse all icon components sourced from app/components/base/icons/src. Use the search bar + to filter by name or path. +

+
+ setQuery(event.target.value)} + /> + {filtered.length} icons + +
+
+ {categoryOrder.length === 0 && ( +

No icons match the current filter.

+ )} + {categoryOrder.map(category => ( +
+

{category}

+
+ {grouped[category].map(entry => ( +
+
+ +
+ +
+ ))} +
+
+ ))} +
+ ) +} + +const meta: Meta = { + title: 'Base/Icons/Icon Gallery', + component: IconGalleryStory, + parameters: { + layout: 'fullscreen', + }, +} + +export default meta + +type Story = StoryObj + +export const All: Story = { + render: () => , +} diff --git a/web/app/components/base/message-log-modal/index.stories.tsx b/web/app/components/base/message-log-modal/index.stories.tsx index 3dd4b06a55..4173a85ebc 100644 --- a/web/app/components/base/message-log-modal/index.stories.tsx +++ b/web/app/components/base/message-log-modal/index.stories.tsx @@ -6,6 +6,7 @@ import { useStore } from '@/app/components/app/store' import type { WorkflowRunDetailResponse } from '@/models/log' import type { NodeTracing, NodeTracingListResponse } from '@/types/workflow' import { BlockEnum } from '@/app/components/workflow/types' +import { WorkflowContextProvider } from '@/app/components/workflow/context' const SAMPLE_APP_DETAIL = { id: 'app-demo-1', @@ -143,10 +144,12 @@ const MessageLogPreview = (props: MessageLogModalProps) => { return (
- + + +
) } diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx index 17a46febdf..3bd82d59c1 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -24,8 +24,8 @@ import { debounce } from 'lodash-es' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import LogViewer from '../log-viewer' -import { usePluginSubscriptionStore } from '../store' import { usePluginStore } from '../../store' +import { useSubscriptionList } from '../use-subscription-list' type Props = { onClose: () => void @@ -91,7 +91,7 @@ const MultiSteps = ({ currentStep }: { currentStep: ApiKeyStep }) => { export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { const { t } = useTranslation() const detail = usePluginStore(state => state.detail) - const { refresh } = usePluginSubscriptionStore() + const { refetch } = useSubscriptionList() const [currentStep, setCurrentStep] = useState(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration) @@ -295,7 +295,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { message: t('pluginTrigger.subscription.createSuccess'), }) onClose() - refresh?.() + refetch?.() }, onError: async (error: any) => { const errorMessage = await parsePluginErrorMessage(error) || t('pluginTrigger.subscription.createFailed') diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx index 178983c6b1..5f4e8a2cbf 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx @@ -4,7 +4,7 @@ import Toast from '@/app/components/base/toast' import { useDeleteTriggerSubscription } from '@/service/use-triggers' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import { usePluginSubscriptionStore } from './store' +import { useSubscriptionList } from './use-subscription-list' type Props = { onClose: (deleted: boolean) => void @@ -18,7 +18,7 @@ const tPrefix = 'pluginTrigger.subscription.list.item.actions.deleteConfirm' export const DeleteConfirm = (props: Props) => { const { onClose, isShow, currentId, currentName, workflowsInUse } = props - const { refresh } = usePluginSubscriptionStore() + const { refetch } = useSubscriptionList() const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription() const { t } = useTranslation() const [inputName, setInputName] = useState('') @@ -40,7 +40,7 @@ export const DeleteConfirm = (props: Props) => { message: t(`${tPrefix}.success`, { name: currentName }), className: 'z-[10000001]', }) - refresh?.() + refetch?.() onClose(true) }, onError: (error: any) => { diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/store.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/store.ts deleted file mode 100644 index 24840e9971..0000000000 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/store.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { create } from 'zustand' - -type ShapeSubscription = { - refresh?: () => void - setRefresh: (refresh: () => void) => void -} - -export const usePluginSubscriptionStore = create(set => ({ - refresh: undefined, - setRefresh: (refresh: () => void) => set({ refresh }), -})) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list.ts index ff3e903a31..9f95ff05a0 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list.ts +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/use-subscription-list.ts @@ -1,19 +1,11 @@ -import { useEffect } from 'react' import { useTriggerSubscriptions } from '@/service/use-triggers' import { usePluginStore } from '../store' -import { usePluginSubscriptionStore } from './store' export const useSubscriptionList = () => { const detail = usePluginStore(state => state.detail) - const { setRefresh } = usePluginSubscriptionStore() const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(detail?.provider || '') - useEffect(() => { - if (refetch) - setRefresh(refetch) - }, [refetch, setRefresh]) - return { detail, subscriptions, diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx index 1f40b1e4b3..470a59f47a 100644 --- a/web/app/components/tools/mcp/mcp-service-card.tsx +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -30,10 +30,14 @@ import { useDocLink } from '@/context/i18n' export type IAppCardProps = { appInfo: AppDetailResponse & Partial + triggerModeDisabled?: boolean // align with Trigger Node vs User Input exclusivity + triggerModeMessage?: React.ReactNode // display-only message explaining the trigger restriction } function MCPServiceCard({ appInfo, + triggerModeDisabled = false, + triggerModeMessage = '', }: IAppCardProps) { const { t } = useTranslation() const docLink = useDocLink() @@ -79,7 +83,7 @@ function MCPServiceCard({ const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start) const missingStartNode = isWorkflowApp && !hasStartNode const hasInsufficientPermissions = !isCurrentWorkspaceEditor - const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode + const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode || triggerModeDisabled const isMinimalState = appUnpublished || missingStartNode const [activated, setActivated] = useState(serverActivated) @@ -144,7 +148,18 @@ function MCPServiceCard({ return ( <>
-
+
+ {triggerModeDisabled && ( + triggerModeMessage ? ( + + + + ) : + )}
@@ -182,7 +197,7 @@ function MCPServiceCard({ {t('appOverview.overview.appInfo.enableTooltip.learnMore')}
- ) : '' + ) : triggerModeMessage || '' ) : '' } position="right" diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 8c4ec7299e..be7aabbc68 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -249,6 +249,8 @@ export const useChecklistBeforePublish = () => { const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() + const appMode = useAppStore.getState().appDetail?.mode + const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => { let checkData = data @@ -366,17 +368,22 @@ export const useChecklistBeforePublish = () => { } } - if (!validNodes.find(n => n.id === node.id)) { + const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false + const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true + const isUnconnected = !validNodes.find(n => n.id === node.id) + + if (isUnconnected && !canSkipConnectionCheck) { notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnectTip')}` }) return false } } - const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum)) - - if (startNodesFiltered.length === 0) { - notify({ type: 'error', message: t('workflow.common.needStartNode') }) - return false + if (shouldCheckStartNode) { + const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum)) + if (startNodesFiltered.length === 0) { + notify({ type: 'error', message: t('workflow.common.needStartNode') }) + return false + } } const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired) @@ -391,7 +398,7 @@ export const useChecklistBeforePublish = () => { } return true - }, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools]) + }, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode]) return { handleCheckBeforePublish, diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index eaafab550e..bc33a05f58 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -298,7 +298,7 @@ const BasePanel: FC = ({ const { setDetail } = usePluginStore() useEffect(() => { - if (currentTriggerPlugin?.subscription_constructor) { + if (currentTriggerPlugin) { setDetail({ name: currentTriggerPlugin.label[language], plugin_id: currentTriggerPlugin.plugin_id || '', diff --git a/web/i18n/en-US/app-overview.ts b/web/i18n/en-US/app-overview.ts index 4e88840b6d..20730636f4 100644 --- a/web/i18n/en-US/app-overview.ts +++ b/web/i18n/en-US/app-overview.ts @@ -138,6 +138,9 @@ const translation = { running: 'In Service', disable: 'Disabled', }, + disableTooltip: { + triggerMode: 'The {{feature}} feature is not supported in Trigger Node mode.', + }, }, analysis: { title: 'Analysis', diff --git a/web/i18n/ja-JP/app-overview.ts b/web/i18n/ja-JP/app-overview.ts index ad1abb78fa..8fa05608c5 100644 --- a/web/i18n/ja-JP/app-overview.ts +++ b/web/i18n/ja-JP/app-overview.ts @@ -138,6 +138,9 @@ const translation = { running: '稼働中', disable: '無効', }, + disableTooltip: { + triggerMode: 'トリガーノードモードでは{{feature}}機能を使用できません。', + }, }, analysis: { title: '分析', diff --git a/web/i18n/zh-Hans/app-overview.ts b/web/i18n/zh-Hans/app-overview.ts index 730240b9f7..2b9379e51b 100644 --- a/web/i18n/zh-Hans/app-overview.ts +++ b/web/i18n/zh-Hans/app-overview.ts @@ -138,6 +138,9 @@ const translation = { running: '运行中', disable: '已停用', }, + disableTooltip: { + triggerMode: '触发节点模式下不支持{{feature}}功能。', + }, }, analysis: { title: '分析',