From c36173f5a9a3e9850909178a69e29d9f738fe731 Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Wed, 22 Oct 2025 11:55:26 +0800 Subject: [PATCH 01/11] fix: typing --- api/core/app/engine_layers/timeslice_layer.py | 6 +++--- api/tasks/async_workflow_tasks.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/core/app/engine_layers/timeslice_layer.py b/api/core/app/engine_layers/timeslice_layer.py index de5129de55..9c23528714 100644 --- a/api/core/app/engine_layers/timeslice_layer.py +++ b/api/core/app/engine_layers/timeslice_layer.py @@ -13,7 +13,7 @@ from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand logger = logging.getLogger(__name__) -class TimesliceLayer(GraphEngineLayer): +class TimeSliceLayer(GraphEngineLayer): """ CFS plan scheduler to control the timeslice of the workflow. """ @@ -25,8 +25,8 @@ class TimesliceLayer(GraphEngineLayer): CFS plan scheduler allows to control the timeslice of the workflow. """ - if not TimesliceLayer.scheduler.running: - TimesliceLayer.scheduler.start() + if not TimeSliceLayer.scheduler.running: + TimeSliceLayer.scheduler.start() super().__init__() self.cfs_plan_scheduler = cfs_plan_scheduler diff --git a/api/tasks/async_workflow_tasks.py b/api/tasks/async_workflow_tasks.py index 14b289a6a7..111d2dee0c 100644 --- a/api/tasks/async_workflow_tasks.py +++ b/api/tasks/async_workflow_tasks.py @@ -13,7 +13,7 @@ from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker from core.app.apps.workflow.app_generator import WorkflowAppGenerator -from core.app.engine_layers.timeslice_layer import TimesliceLayer +from core.app.engine_layers.timeslice_layer import TimeSliceLayer from core.app.engine_layers.trigger_post_layer import TriggerPostLayer from core.app.entities.app_invoke_entities import InvokeFrom from extensions.ext_database import db @@ -140,7 +140,7 @@ def _execute_workflow_common( triggered_from=trigger_data.trigger_type, root_node_id=trigger_data.root_node_id, layers=[ - TimesliceLayer(cfs_plan_scheduler), + TimeSliceLayer(cfs_plan_scheduler), TriggerPostLayer(cfs_plan_scheduler_entity, start_time, trigger_log.id), ], ) From 7f70d1de1caaa7c59c54fd5a3fa108e5e08440cd Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Wed, 22 Oct 2025 12:10:12 +0800 Subject: [PATCH 02/11] ASYNC_WORKFLOW_SCHEDULER_GRANULARITY --- api/configs/feature/__init__.py | 2 +- api/core/app/engine_layers/suspend_layer.py | 7 ++++++- api/core/app/engine_layers/timeslice_layer.py | 5 +---- api/tasks/async_workflow_tasks.py | 13 ++++++++++--- api/tasks/workflow_cfs_scheduler/cfs_scheduler.py | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index bd58132839..f5262a2f8b 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -196,7 +196,7 @@ class AsyncWorkflowConfig(BaseSettings): "to avoid this, workflow can be suspended if needed, to achieve" "this, a time-based checker is required, every granularity seconds, " "the checker will check the workflow queue and suspend the workflow", - default=1, + default=60, ge=1, ) diff --git a/api/core/app/engine_layers/suspend_layer.py b/api/core/app/engine_layers/suspend_layer.py index f981523149..0a107de012 100644 --- a/api/core/app/engine_layers/suspend_layer.py +++ b/api/core/app/engine_layers/suspend_layer.py @@ -1,5 +1,6 @@ from core.workflow.graph_engine.layers.base import GraphEngineLayer from core.workflow.graph_events.base import GraphEngineEvent +from core.workflow.graph_events.graph import GraphRunPausedEvent class SuspendLayer(GraphEngineLayer): @@ -9,7 +10,11 @@ class SuspendLayer(GraphEngineLayer): pass def on_event(self, event: GraphEngineEvent): - pass + """ + Handle the paused event, stash runtime state into storage and wait for resume. + """ + if isinstance(event, GraphRunPausedEvent): + pass def on_graph_end(self, error: Exception | None): """ """ diff --git a/api/core/app/engine_layers/timeslice_layer.py b/api/core/app/engine_layers/timeslice_layer.py index 9c23528714..4f47379a40 100644 --- a/api/core/app/engine_layers/timeslice_layer.py +++ b/api/core/app/engine_layers/timeslice_layer.py @@ -4,7 +4,6 @@ from typing import ClassVar from apscheduler.schedulers.background import BackgroundScheduler # type: ignore -from configs import dify_config from core.workflow.graph_engine.entities.commands import CommandType, GraphEngineCommand from core.workflow.graph_engine.layers.base import GraphEngineLayer from core.workflow.graph_events.base import GraphEngineEvent @@ -70,9 +69,7 @@ class TimeSliceLayer(GraphEngineLayer): except Exception: logger.exception("scheduler error during check if the workflow need to be suspended") - self.scheduler.add_job( - runner, "interval", seconds=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, id=schedule_id - ) + self.scheduler.add_job(runner, "interval", seconds=self.cfs_plan_scheduler.plan.granularity, id=schedule_id) def on_event(self, event: GraphEngineEvent): pass diff --git a/api/tasks/async_workflow_tasks.py b/api/tasks/async_workflow_tasks.py index 111d2dee0c..5a8274929a 100644 --- a/api/tasks/async_workflow_tasks.py +++ b/api/tasks/async_workflow_tasks.py @@ -12,6 +12,7 @@ from celery import shared_task from sqlalchemy import select from sqlalchemy.orm import Session, sessionmaker +from configs import dify_config from core.app.apps.workflow.app_generator import WorkflowAppGenerator from core.app.engine_layers.timeslice_layer import TimeSliceLayer from core.app.engine_layers.trigger_post_layer import TriggerPostLayer @@ -38,7 +39,9 @@ def execute_workflow_professional(task_data_dict: dict[str, Any]): """Execute workflow for professional tier with highest priority""" task_data = WorkflowTaskData.model_validate(task_data_dict) cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( - queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice + queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE, + schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, + granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, @@ -52,7 +55,9 @@ def execute_workflow_team(task_data_dict: dict[str, Any]): """Execute workflow for team tier""" task_data = WorkflowTaskData.model_validate(task_data_dict) cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( - queue=AsyncWorkflowQueue.TEAM_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice + queue=AsyncWorkflowQueue.TEAM_QUEUE, + schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, + granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, @@ -66,7 +71,9 @@ def execute_workflow_sandbox(task_data_dict: dict[str, Any]): """Execute workflow for free tier with lower retry limit""" task_data = WorkflowTaskData.model_validate(task_data_dict) cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( - queue=AsyncWorkflowQueue.SANDBOX_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice + queue=AsyncWorkflowQueue.SANDBOX_QUEUE, + schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, + granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, diff --git a/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py b/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py index 2a4153dcd2..c7a1650d23 100644 --- a/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py +++ b/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py @@ -24,7 +24,7 @@ class TriggerCFSPlanScheduler(CFSPlanScheduler): if self.plan.queue in [AsyncWorkflowQueue.PROFESSIONAL_QUEUE, AsyncWorkflowQueue.TEAM_QUEUE]: """ - permitted all paid users to schedule the workflow + permitted all paid users to schedule the workflow any time """ return SchedulerCommand.NONE From cb5607fc8c2f8ed232921dd0766721f83eda15f7 Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Wed, 22 Oct 2025 12:13:12 +0800 Subject: [PATCH 03/11] refactor: TimeSliceLayer --- api/core/app/engine_layers/timeslice_layer.py | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/api/core/app/engine_layers/timeslice_layer.py b/api/core/app/engine_layers/timeslice_layer.py index 4f47379a40..8d4491b93c 100644 --- a/api/core/app/engine_layers/timeslice_layer.py +++ b/api/core/app/engine_layers/timeslice_layer.py @@ -31,6 +31,36 @@ class TimeSliceLayer(GraphEngineLayer): self.cfs_plan_scheduler = cfs_plan_scheduler self.stopped = False + def _checker_job(self, schedule_id: str): + """ + Check if the workflow need to be suspended. + """ + try: + if self.stopped: + self.scheduler.remove_job(schedule_id) + return + + if self.cfs_plan_scheduler.can_schedule() == SchedulerCommand.RESOURCE_LIMIT_REACHED: + # remove the job + self.scheduler.remove_job(schedule_id) + + if not self.command_channel: + logger.exception("No command channel to stop the workflow") + return + + # send command to pause the workflow + self.command_channel.send_command( + GraphEngineCommand( + command_type=CommandType.PAUSE, + payload={ + "reason": SchedulerCommand.RESOURCE_LIMIT_REACHED, + }, + ) + ) + + except Exception: + logger.exception("scheduler error during check if the workflow need to be suspended") + def on_graph_start(self): """ Start timer to check if the workflow need to be suspended. @@ -38,38 +68,12 @@ class TimeSliceLayer(GraphEngineLayer): schedule_id = uuid.uuid4().hex - def runner(): - """ - Whenever the workflow is running, keep checking if we need to suspend it. - Otherwise, return directly. - """ - try: - if self.stopped: - self.scheduler.remove_job(schedule_id) - return - - if self.cfs_plan_scheduler.can_schedule() == SchedulerCommand.RESOURCE_LIMIT_REACHED: - # remove the job - self.scheduler.remove_job(schedule_id) - - if not self.command_channel: - logger.exception("No command channel to stop the workflow") - return - - # send command to pause the workflow - self.command_channel.send_command( - GraphEngineCommand( - command_type=CommandType.PAUSE, - payload={ - "reason": SchedulerCommand.RESOURCE_LIMIT_REACHED, - }, - ) - ) - - except Exception: - logger.exception("scheduler error during check if the workflow need to be suspended") - - self.scheduler.add_job(runner, "interval", seconds=self.cfs_plan_scheduler.plan.granularity, id=schedule_id) + self.scheduler.add_job( + lambda: self._checker_job(schedule_id), + "interval", + seconds=self.cfs_plan_scheduler.plan.granularity, + id=schedule_id, + ) def on_event(self, event: GraphEngineEvent): pass From 14acd0584627786a2196e2470d5f8f80e079ae92 Mon Sep 17 00:00:00 2001 From: Yeuoly Date: Wed, 22 Oct 2025 12:41:19 +0800 Subject: [PATCH 04/11] fix --- .../app/engine_layers/trigger_post_layer.py | 4 ++-- api/tasks/async_workflow_tasks.py | 18 +++++++++--------- .../workflow_cfs_scheduler/cfs_scheduler.py | 11 +++++------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/api/core/app/engine_layers/trigger_post_layer.py b/api/core/app/engine_layers/trigger_post_layer.py index 5b31ec2ae1..1309295b1a 100644 --- a/api/core/app/engine_layers/trigger_post_layer.py +++ b/api/core/app/engine_layers/trigger_post_layer.py @@ -11,7 +11,7 @@ from core.workflow.graph_events.graph import GraphRunFailedEvent, GraphRunPaused from models.engine import db from models.enums import WorkflowTriggerStatus from repositories.sqlalchemy_workflow_trigger_log_repository import SQLAlchemyWorkflowTriggerLogRepository -from tasks.workflow_cfs_scheduler.cfs_scheduler import TriggerWorkflowCFSPlanEntity +from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntity logger = logging.getLogger(__name__) @@ -29,7 +29,7 @@ class TriggerPostLayer(GraphEngineLayer): def __init__( self, - cfs_plan_scheduler_entity: TriggerWorkflowCFSPlanEntity, + cfs_plan_scheduler_entity: AsyncWorkflowCFSPlanEntity, start_time: datetime, trigger_log_id: str, ): diff --git a/api/tasks/async_workflow_tasks.py b/api/tasks/async_workflow_tasks.py index 5a8274929a..cce0256a42 100644 --- a/api/tasks/async_workflow_tasks.py +++ b/api/tasks/async_workflow_tasks.py @@ -30,7 +30,7 @@ from services.workflow.entities import ( WorkflowScheduleCFSPlanEntity, WorkflowTaskData, ) -from tasks.workflow_cfs_scheduler.cfs_scheduler import TriggerCFSPlanScheduler, TriggerWorkflowCFSPlanEntity +from tasks.workflow_cfs_scheduler.cfs_scheduler import AsyncWorkflowCFSPlanEntity, AsyncWorkflowCFSPlanScheduler from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue @@ -38,14 +38,14 @@ from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue def execute_workflow_professional(task_data_dict: dict[str, Any]): """Execute workflow for professional tier with highest priority""" task_data = WorkflowTaskData.model_validate(task_data_dict) - cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( + cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity( queue=AsyncWorkflowQueue.PROFESSIONAL_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, - TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity), + AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity), cfs_plan_scheduler_entity, ) @@ -54,14 +54,14 @@ def execute_workflow_professional(task_data_dict: dict[str, Any]): def execute_workflow_team(task_data_dict: dict[str, Any]): """Execute workflow for team tier""" task_data = WorkflowTaskData.model_validate(task_data_dict) - cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( + cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity( queue=AsyncWorkflowQueue.TEAM_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, - TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity), + AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity), cfs_plan_scheduler_entity, ) @@ -70,22 +70,22 @@ def execute_workflow_team(task_data_dict: dict[str, Any]): def execute_workflow_sandbox(task_data_dict: dict[str, Any]): """Execute workflow for free tier with lower retry limit""" task_data = WorkflowTaskData.model_validate(task_data_dict) - cfs_plan_scheduler_entity = TriggerWorkflowCFSPlanEntity( + cfs_plan_scheduler_entity = AsyncWorkflowCFSPlanEntity( queue=AsyncWorkflowQueue.SANDBOX_QUEUE, schedule_strategy=WorkflowScheduleCFSPlanEntity.Strategy.TimeSlice, granularity=dify_config.ASYNC_WORKFLOW_SCHEDULER_GRANULARITY, ) _execute_workflow_common( task_data, - TriggerCFSPlanScheduler(plan=cfs_plan_scheduler_entity), + AsyncWorkflowCFSPlanScheduler(plan=cfs_plan_scheduler_entity), cfs_plan_scheduler_entity, ) def _execute_workflow_common( task_data: WorkflowTaskData, - cfs_plan_scheduler: TriggerCFSPlanScheduler, - cfs_plan_scheduler_entity: TriggerWorkflowCFSPlanEntity, + cfs_plan_scheduler: AsyncWorkflowCFSPlanScheduler, + cfs_plan_scheduler_entity: AsyncWorkflowCFSPlanEntity, ): """Execute workflow with common logic and trigger log updates.""" diff --git a/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py b/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py index c7a1650d23..218e61f6d9 100644 --- a/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py +++ b/api/tasks/workflow_cfs_scheduler/cfs_scheduler.py @@ -3,7 +3,7 @@ from services.workflow.scheduler import CFSPlanScheduler, SchedulerCommand from tasks.workflow_cfs_scheduler.entities import AsyncWorkflowQueue -class TriggerWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity): +class AsyncWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity): """ Trigger workflow CFS plan entity. """ @@ -11,17 +11,17 @@ class TriggerWorkflowCFSPlanEntity(WorkflowScheduleCFSPlanEntity): queue: AsyncWorkflowQueue -class TriggerCFSPlanScheduler(CFSPlanScheduler): +class AsyncWorkflowCFSPlanScheduler(CFSPlanScheduler): """ Trigger workflow CFS plan scheduler. """ + plan: AsyncWorkflowCFSPlanEntity + def can_schedule(self) -> SchedulerCommand: """ Check if the workflow can be scheduled. """ - assert isinstance(self.plan, TriggerWorkflowCFSPlanEntity) - if self.plan.queue in [AsyncWorkflowQueue.PROFESSIONAL_QUEUE, AsyncWorkflowQueue.TEAM_QUEUE]: """ permitted all paid users to schedule the workflow any time @@ -29,5 +29,4 @@ class TriggerCFSPlanScheduler(CFSPlanScheduler): return SchedulerCommand.NONE # FIXME: avoid the sandbox user's workflow at a running state for ever - - return SchedulerCommand.NONE + return SchedulerCommand.RESOURCE_LIMIT_REACHED From 28fe58f3ddefe0b9136c5e0a6a4409a83d9cdf1f Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Wed, 22 Oct 2025 10:36:19 +0800 Subject: [PATCH 05/11] feat: try to add tools suggestions --- .../workflow/block-selector/all-tools.tsx | 54 ++++ .../block-selector/featured-tools.tsx | 283 ++++++++++++++++++ web/i18n/en-US/workflow.ts | 6 + web/i18n/zh-Hans/workflow.ts | 6 + web/service/use-plugins.ts | 30 ++ 5 files changed, 379 insertions(+) create mode 100644 web/app/components/workflow/block-selector/featured-tools.tsx diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index dabb8f4a9a..d86795826a 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -29,6 +29,9 @@ import { PluginCategoryEnum } from '../../plugins/types' import { useMarketplacePlugins } from '../../plugins/marketplace/hooks' import { useGlobalPublicStore } from '@/context/global-public-context' import RAGToolSuggestions from './rag-tool-suggestions' +import FeaturedTools from './featured-tools' +import { useCheckInstalled, useRecommendedMarketplacePlugins } from '@/service/use-plugins' +import { useInvalidateAllBuiltInTools } from '@/service/use-tools' import Link from 'next/link' type AllToolsProps = { @@ -80,6 +83,16 @@ const AllTools = ({ const isMatchingKeywords = (text: string, keywords: string) => { return text.toLowerCase().includes(keywords.toLowerCase()) } + const allProviders = useMemo(() => [...buildInTools, ...customTools, ...workflowTools, ...mcpTools], [buildInTools, customTools, workflowTools, mcpTools]) + const providerMap = useMemo(() => { + const map = new Map() + allProviders.forEach((provider) => { + const key = provider.plugin_id || provider.id + if (key) + map.set(key, provider) + }) + return map + }, [allProviders]) const tools = useMemo(() => { let mergedTools: ToolWithProvider[] = [] if (activeTab === ToolTypeEnum.All) @@ -136,6 +149,27 @@ const AllTools = ({ } = useMarketplacePlugins() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + const { + data: recommendedPlugins = [], + isLoading: isLoadingRecommended, + } = useRecommendedMarketplacePlugins({ + enabled: enable_marketplace, + }) + const recommendedPluginIds = useMemo( + () => recommendedPlugins.map(plugin => plugin.plugin_id), + [recommendedPlugins], + ) + const installedCheck = useCheckInstalled({ + pluginIds: recommendedPluginIds, + enabled: recommendedPluginIds.length > 0, + }) + const installedPluginIds = useMemo( + () => new Set(installedCheck.data?.plugins.map(plugin => plugin.plugin_id) ?? []), + [installedCheck.data], + ) + const loadingRecommendedInstallStatus = installedCheck.isLoading || installedCheck.isRefetching + const invalidateBuiltInTools = useInvalidateAllBuiltInTools() + useEffect(() => { if (!enable_marketplace) return if (hasFilter) { @@ -155,6 +189,11 @@ const AllTools = ({ const hasToolsContent = tools.length > 0 const hasPluginContent = enable_marketplace && notInstalledPlugins.length > 0 const shouldShowEmptyState = hasFilter && !hasToolsContent && !hasPluginContent + const shouldShowFeatured = enable_marketplace + && activeTab === ToolTypeEnum.All + && !hasFilter + && !isLoadingRecommended + && recommendedPlugins.length > 0 return (
@@ -193,6 +232,21 @@ const AllTools = ({ onTagsChange={onTagsChange} /> )} + {shouldShowFeatured && ( + { + invalidateBuiltInTools() + await installedCheck.refetch() + }} + /> + )} + onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void + selectedTools?: ToolValue[] + canChooseMCPTool?: boolean + installedPluginIds: Set + loadingInstalledStatus: boolean + onInstallSuccess?: () => void +} + +function isToolSelected(tool: Tool, provider: ToolWithProvider, selectedTools?: ToolValue[]): boolean { + if (!selectedTools || !selectedTools.length) + return false + return selectedTools.some(item => (item.provider_name === provider.name || item.provider_name === provider.id) && item.tool_name === tool.name) +} + +const FeaturedTools = ({ + plugins, + providerMap, + onSelect, + selectedTools, + canChooseMCPTool, + installedPluginIds, + loadingInstalledStatus, + onInstallSuccess, +}: FeaturedToolsProps) => { + const { t } = useTranslation() + const language = useGetLanguage() + const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) + const [installingIdentifier, setInstallingIdentifier] = useState(null) + const installMutation = useInstallPackageFromMarketPlace({ + onSuccess: () => { + onInstallSuccess?.() + }, + onSettled: () => { + setInstallingIdentifier(null) + }, + }) + + useEffect(() => { + setVisibleCount(INITIAL_VISIBLE_COUNT) + }, [plugins]) + + const visiblePlugins = useMemo( + () => plugins.slice(0, Math.min(MAX_RECOMMENDED_COUNT, visibleCount)), + [plugins, visibleCount], + ) + + if (!visiblePlugins.length) + return null + + const showMore = visibleCount < Math.min(MAX_RECOMMENDED_COUNT, plugins.length) + + return ( +
+
+ {t('workflow.tabs.featuredTools')} +
+
+ {visiblePlugins.map(plugin => renderFeaturedToolItem({ + plugin, + providerMap, + installedPluginIds, + installMutationPending: installMutation.isPending, + installingIdentifier, + loadingInstalledStatus, + canChooseMCPTool, + onSelect, + selectedTools, + language, + installPlugin: installMutation.mutate, + setInstallingIdentifier, + }))} +
+ {showMore && ( + + )} +
+ ) +} + +type FeaturedToolItemProps = { + plugin: Plugin + provider: ToolWithProvider | undefined + isInstalled: boolean + installDisabled: boolean + canChooseMCPTool?: boolean + onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void + selectedTools?: ToolValue[] + language: string + onInstall: () => void + isInstalling: boolean +} + +function FeaturedToolItem({ + plugin, + provider, + isInstalled, + installDisabled, + canChooseMCPTool, + onSelect, + selectedTools, + language, + onInstall, + isInstalling, +}: FeaturedToolItemProps) { + const { t } = useTranslation() + const [isExpanded, setExpanded] = useState(false) + const hasProvider = Boolean(provider) + const installCountLabel = t('plugin.install', { num: plugin.install_count?.toLocaleString() ?? 0 }) + const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief + + useEffect(() => { + if (!hasProvider) + setExpanded(false) + }, [hasProvider]) + + let toggleLabel: string + if (!hasProvider) + toggleLabel = t('workflow.common.syncingData') + else if (isExpanded) + toggleLabel = t('workflow.tabs.hideActions') + else + toggleLabel = t('workflow.tabs.usePlugin') + + return ( +
+
+ +
+
+
+ {plugin.label?.[language] || plugin.name} +
+ {isInstalled && ( + + {t('workflow.tabs.installed')} + + )} +
+
+ {description} +
+
+ {installCountLabel} + {plugin.org && {t('workflow.tabs.pluginByAuthor', { author: plugin.org })}} +
+
+
+ {!isInstalled && ( + + )} + {isInstalled && ( + + )} +
+
+ {isInstalled && hasProvider && isExpanded && ( +
+ {provider.tools.map((tool) => { + const isSelected = isToolSelected(tool, provider, selectedTools) + const isMCPTool = provider.type === CollectionType.mcp + const disabled = isSelected || (!canChooseMCPTool && isMCPTool) + + return ( + + ) + })} +
+ )} +
+ ) +} + +type RenderFeaturedToolParams = { + plugin: Plugin + providerMap: Map + installedPluginIds: Set + installMutationPending: boolean + installingIdentifier: string | null + loadingInstalledStatus: boolean + canChooseMCPTool?: boolean + onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void + selectedTools?: ToolValue[] + language: string + installPlugin: (uniqueIdentifier: string) => void + setInstallingIdentifier: (identifier: string | null) => void +} + +function renderFeaturedToolItem({ + plugin, + providerMap, + installedPluginIds, + installMutationPending, + installingIdentifier, + loadingInstalledStatus, + canChooseMCPTool, + onSelect, + selectedTools, + language, + installPlugin, + setInstallingIdentifier, +}: RenderFeaturedToolParams) { + const provider = providerMap.get(plugin.plugin_id) + const isInstalled = installedPluginIds.has(plugin.plugin_id) + const isInstalling = installMutationPending && installingIdentifier === plugin.latest_package_identifier + + return ( + { + if (installMutationPending) + return + setInstallingIdentifier(plugin.latest_package_identifier) + installPlugin(plugin.latest_package_identifier) + }} + isInstalling={isInstalling} + /> + ) +} + +export default FeaturedTools diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index a967a82bb2..e26ff5e859 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -277,6 +277,12 @@ const translation = { 'addAll': 'Add all', 'sources': 'Sources', 'searchDataSource': 'Search Data Source', + 'featuredTools': 'Featured', + 'showMoreFeatured': 'Show more', + 'installed': 'Installed', + 'pluginByAuthor': 'By {{author}}', + 'usePlugin': 'Select tool', + 'hideActions': 'Hide tools', }, blocks: { 'start': 'User Input', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index a7206db9df..90a2d915b9 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -263,6 +263,12 @@ const translation = { 'sources': '数据源', 'searchDataSource': '搜索数据源', 'start': '开始', + 'featuredTools': '精选推荐', + 'showMoreFeatured': '查看更多', + 'installed': '已安装', + 'pluginByAuthor': '来自 {{author}}', + 'usePlugin': '选择工具', + 'hideActions': '收起工具', }, blocks: { 'start': '用户输入', diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 21a3cc00fd..fcd626b58e 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -43,6 +43,7 @@ import useReferenceSetting from '@/app/components/plugins/plugin-page/use-refere import { uninstallPlugin } from '@/service/plugins' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { cloneDeep } from 'lodash-es' +import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' const NAME_SPACE = 'plugins' @@ -66,6 +67,35 @@ export const useCheckInstalled = ({ }) } +const useRecommendedMarketplacePluginsKey = [NAME_SPACE, 'recommendedMarketplacePlugins'] +export const useRecommendedMarketplacePlugins = ({ + category = PluginCategoryEnum.tool, + enabled = true, + limit = 15, +}: { + category?: string + enabled?: boolean + limit?: number +} = {}) => { + return useQuery({ + queryKey: [...useRecommendedMarketplacePluginsKey, category, limit], + queryFn: async () => { + const response = await postMarketplace<{ data: { plugins: Plugin[] } }>( + '/collections/__recommended-plugins-overall/plugins', + { + body: { + category, + limit, + }, + }, + ) + return response.data.plugins.map(plugin => getFormattedPlugin(plugin)) + }, + enabled, + staleTime: 60 * 1000, + }) +} + export const useInstalledPluginList = (disable?: boolean, pageSize = 100) => { const fetchPlugins = async ({ pageParam = 1 }) => { const response = await get( From f179b03d6ec7b789b965f5e8815e185cb2c216b1 Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Wed, 22 Oct 2025 11:56:13 +0800 Subject: [PATCH 06/11] fix: constrain rag pipeline datasource selector width --- web/app/components/workflow/block-selector/data-sources.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/workflow/block-selector/data-sources.tsx b/web/app/components/workflow/block-selector/data-sources.tsx index b2b6ad92cf..3961f63dbe 100644 --- a/web/app/components/workflow/block-selector/data-sources.tsx +++ b/web/app/components/workflow/block-selector/data-sources.tsx @@ -92,10 +92,10 @@ const DataSources = ({ }, [searchText, enable_marketplace]) return ( -
+
Date: Wed, 22 Oct 2025 12:14:05 +0800 Subject: [PATCH 07/11] feat: suggestions ui --- .../workflow/block-selector/all-tools.tsx | 4 +- .../block-selector/featured-tools.tsx | 117 +++++++++++++----- 2 files changed, 86 insertions(+), 35 deletions(-) diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index d86795826a..e071c665ae 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -153,7 +153,7 @@ const AllTools = ({ data: recommendedPlugins = [], isLoading: isLoadingRecommended, } = useRecommendedMarketplacePlugins({ - enabled: enable_marketplace, + enabled: enable_marketplace && !isInRAGPipeline, }) const recommendedPluginIds = useMemo( () => recommendedPlugins.map(plugin => plugin.plugin_id), @@ -193,6 +193,7 @@ const AllTools = ({ && activeTab === ToolTypeEnum.All && !hasFilter && !isLoadingRecommended + && !isInRAGPipeline && recommendedPlugins.length > 0 return ( @@ -241,6 +242,7 @@ const AllTools = ({ canChooseMCPTool={canChooseMCPTool} installedPluginIds={installedPluginIds} loadingInstalledStatus={loadingRecommendedInstallStatus} + isLoading={isLoadingRecommended} onInstallSuccess={async () => { invalidateBuiltInTools() await installedCheck.refetch() diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index ba58a34f89..1ae3d9fe22 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -10,8 +10,11 @@ import ActionItem from './tool/action-item' import type { Tool } from '@/app/components/tools/types' import { CollectionType } from '@/app/components/tools/types' import BlockIcon from '../block-icon' -import { RiArrowDownSLine, RiArrowUpSLine, RiLoader2Line } from '@remixicon/react' +import { RiArrowDownSLine, RiArrowRightSLine, RiArrowUpSLine, RiLoader2Line } from '@remixicon/react' import { useInstallPackageFromMarketPlace } from '@/service/use-plugins' +import Loading from '@/app/components/base/loading' +import Link from 'next/link' +import { getMarketplaceUrl } from '@/utils/var' const MAX_RECOMMENDED_COUNT = 15 const INITIAL_VISIBLE_COUNT = 5 @@ -24,6 +27,7 @@ type FeaturedToolsProps = { canChooseMCPTool?: boolean installedPluginIds: Set loadingInstalledStatus: boolean + isLoading?: boolean onInstallSuccess?: () => void } @@ -33,6 +37,8 @@ function isToolSelected(tool: Tool, provider: ToolWithProvider, selectedTools?: return selectedTools.some(item => (item.provider_name === provider.name || item.provider_name === provider.id) && item.tool_name === tool.name) } +const STORAGE_KEY = 'workflow_tools_featured_collapsed' + const FeaturedTools = ({ plugins, providerMap, @@ -41,12 +47,15 @@ const FeaturedTools = ({ canChooseMCPTool, installedPluginIds, loadingInstalledStatus, + isLoading = false, onInstallSuccess, }: FeaturedToolsProps) => { const { t } = useTranslation() const language = useGetLanguage() const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT) const [installingIdentifier, setInstallingIdentifier] = useState(null) + const [isCollapsed, setIsCollapsed] = useState(false) + const installMutation = useInstallPackageFromMarketPlace({ onSuccess: () => { onInstallSuccess?.() @@ -56,6 +65,20 @@ const FeaturedTools = ({ }, }) + useEffect(() => { + if (typeof window === 'undefined') + return + const stored = window.localStorage.getItem(STORAGE_KEY) + if (stored !== null) + setIsCollapsed(stored === 'true') + }, []) + + useEffect(() => { + if (typeof window === 'undefined') + return + window.localStorage.setItem(STORAGE_KEY, String(isCollapsed)) + }, [isCollapsed]) + useEffect(() => { setVisibleCount(INITIAL_VISIBLE_COUNT) }, [plugins]) @@ -65,43 +88,69 @@ const FeaturedTools = ({ [plugins, visibleCount], ) - if (!visiblePlugins.length) - return null - const showMore = visibleCount < Math.min(MAX_RECOMMENDED_COUNT, plugins.length) + const isMutating = installMutation.isPending + const showEmptyState = !isLoading && !visiblePlugins.length return (
-
- {t('workflow.tabs.featuredTools')} -
-
- {visiblePlugins.map(plugin => renderFeaturedToolItem({ - plugin, - providerMap, - installedPluginIds, - installMutationPending: installMutation.isPending, - installingIdentifier, - loadingInstalledStatus, - canChooseMCPTool, - onSelect, - selectedTools, - language, - installPlugin: installMutation.mutate, - setInstallingIdentifier, - }))} -
- {showMore && ( - + + + {!isCollapsed && ( + <> + {isLoading && ( +
+ +
+ )} + + {showEmptyState && ( +

+ + {t('workflow.tabs.noFeaturedPlugins')} + +

+ )} + + {!isLoading && visiblePlugins.length > 0 && ( +
+ {visiblePlugins.map(plugin => renderFeaturedToolItem({ + plugin, + providerMap, + installedPluginIds, + installMutationPending: isMutating, + installingIdentifier, + loadingInstalledStatus, + canChooseMCPTool, + onSelect, + selectedTools, + language, + installPlugin: installMutation.mutate, + setInstallingIdentifier, + }))} +
+ )} + + {!isLoading && visiblePlugins.length > 0 && showMore && ( + + )} + )}
) From d99644237bd7052f9768282910619e033dabce90 Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Wed, 22 Oct 2025 12:23:33 +0800 Subject: [PATCH 08/11] chore: align help link translations --- web/i18n/de-DE/workflow.ts | 2 +- web/i18n/en-US/workflow.ts | 2 +- web/i18n/es-ES/workflow.ts | 2 +- web/i18n/fa-IR/workflow.ts | 2 +- web/i18n/fr-FR/workflow.ts | 2 +- web/i18n/hi-IN/workflow.ts | 2 +- web/i18n/id-ID/workflow.ts | 2 +- web/i18n/it-IT/workflow.ts | 2 +- web/i18n/ja-JP/workflow.ts | 2 +- web/i18n/ko-KR/workflow.ts | 2 +- web/i18n/pl-PL/workflow.ts | 2 +- web/i18n/pt-BR/workflow.ts | 2 +- web/i18n/ro-RO/workflow.ts | 2 +- web/i18n/ru-RU/workflow.ts | 2 +- web/i18n/sl-SI/workflow.ts | 2 +- web/i18n/th-TH/workflow.ts | 2 +- web/i18n/tr-TR/workflow.ts | 2 +- web/i18n/uk-UA/workflow.ts | 2 +- web/i18n/vi-VN/workflow.ts | 2 +- web/i18n/zh-Hans/workflow.ts | 2 +- web/i18n/zh-Hant/workflow.ts | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index 8b1bb5dd5a..fd9ad92dae 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Benutzereingabefeld', - helpLink: 'Hilfelink', + helpLink: 'Hilfe', about: 'Über', createdBy: 'Erstellt von ', nextStep: 'Nächster Schritt', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index e26ff5e859..30ea04d6ae 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -372,7 +372,7 @@ const translation = { panel: { userInputField: 'User Input Field', changeBlock: 'Change Node', - helpLink: 'Help Link', + helpLink: 'Docs', about: 'About', createdBy: 'Created By ', nextStep: 'Next Step', diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index 05bee4516e..cd66ac112a 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Campo de entrada del usuario', - helpLink: 'Enlace de ayuda', + helpLink: 'Ayuda', about: 'Acerca de', createdBy: 'Creado por ', nextStep: 'Siguiente paso', diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts index 0efe0752fe..c449de8d87 100644 --- a/web/i18n/fa-IR/workflow.ts +++ b/web/i18n/fa-IR/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'فیلد ورودی کاربر', - helpLink: 'لینک کمک', + helpLink: 'راهنما', about: 'درباره', createdBy: 'ساخته شده توسط', nextStep: 'مرحله بعدی', diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index 696f691fc7..41548d5556 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Champ de saisie de l\'utilisateur', - helpLink: 'Lien d\'aide', + helpLink: 'Aide', about: 'À propos', createdBy: 'Créé par', nextStep: 'Étape suivante', diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index 97ccdb2d06..1f076d3306 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -320,7 +320,7 @@ const translation = { }, panel: { userInputField: 'उपयोगकर्ता इनपुट फ़ील्ड', - helpLink: 'सहायता लिंक', + helpLink: 'सहायता', about: 'के बारे में', createdBy: 'द्वारा बनाया गया ', nextStep: 'अगला कदम', diff --git a/web/i18n/id-ID/workflow.ts b/web/i18n/id-ID/workflow.ts index b2e250993a..50aafe4510 100644 --- a/web/i18n/id-ID/workflow.ts +++ b/web/i18n/id-ID/workflow.ts @@ -321,7 +321,7 @@ const translation = { userInputField: 'Bidang Input Pengguna', checklistResolved: 'Semua masalah terselesaikan', createdBy: 'Dibuat oleh', - helpLink: 'Tautan Bantuan', + helpLink: 'Docs', changeBlock: 'Ubah Node', runThisStep: 'Jalankan langkah ini', maximize: 'Maksimalkan Kanvas', diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index 875e6230d9..4d18fd3c6e 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -323,7 +323,7 @@ const translation = { }, panel: { userInputField: 'Campo di Input Utente', - helpLink: 'Link di Aiuto', + helpLink: 'Aiuto', about: 'Informazioni', createdBy: 'Creato da ', nextStep: 'Prossimo Passo', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index cd78ec16b6..5dfb3d983a 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -349,7 +349,7 @@ const translation = { panel: { userInputField: 'ユーザー入力欄', changeBlock: 'ノード変更', - helpLink: 'ヘルプリンク', + helpLink: 'ヘルプ', about: '詳細', createdBy: '作成者', nextStep: '次のステップ', diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index 6ea64df36b..be6edfc655 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -330,7 +330,7 @@ const translation = { }, panel: { userInputField: '사용자 입력 필드', - helpLink: '도움말 링크', + helpLink: '도움말 센터', about: '정보', createdBy: '작성자 ', nextStep: '다음 단계', diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index 0dd6020498..0b8329aa0c 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Pole wprowadzania użytkownika', - helpLink: 'Link do pomocy', + helpLink: 'Pomoc', about: 'O', createdBy: 'Stworzone przez ', nextStep: 'Następny krok', diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index 36ae2a95d2..fecac8509f 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Campo de entrada do usuário', - helpLink: 'Link de ajuda', + helpLink: 'Ajuda', about: 'Sobre', createdBy: 'Criado por ', nextStep: 'Próximo passo', diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index b22cd44634..e4af9b501e 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Câmp de introducere utilizator', - helpLink: 'Link de ajutor', + helpLink: 'Ajutor', about: 'Despre', createdBy: 'Creat de ', nextStep: 'Pasul următor', diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index 6ae8b6d542..31f1b96b41 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Поле ввода пользователя', - helpLink: 'Ссылка на справку', + helpLink: 'Помощь', about: 'О программе', createdBy: 'Создано ', nextStep: 'Следующий шаг', diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts index 9e0201b1b3..1eba5c8520 100644 --- a/web/i18n/sl-SI/workflow.ts +++ b/web/i18n/sl-SI/workflow.ts @@ -324,7 +324,7 @@ const translation = { addNextStep: 'Dodajte naslednji korak v ta delovni potek', checklistTip: 'Prepričajte se, da so vse težave rešene, preden objavite.', selectNextStep: 'Izberi naslednji korak', - helpLink: 'Pomočna povezava', + helpLink: 'Pomoč', checklist: 'Kontrolni seznam', checklistResolved: 'Vse težave so rešene', createdBy: 'Ustvarjeno z', diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts index 97bb63952d..42eadb1789 100644 --- a/web/i18n/th-TH/workflow.ts +++ b/web/i18n/th-TH/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'ฟิลด์ป้อนข้อมูลของผู้ใช้', - helpLink: 'ลิงค์ช่วยเหลือ', + helpLink: 'วิธีใช้', about: 'ประมาณ', createdBy: 'สร้างโดย', nextStep: 'ขั้นตอนถัดไป', diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts index f68d081bba..442ce26567 100644 --- a/web/i18n/tr-TR/workflow.ts +++ b/web/i18n/tr-TR/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Kullanıcı Giriş Alanı', - helpLink: 'Yardım Linki', + helpLink: 'Yardım', about: 'Hakkında', createdBy: 'Oluşturan: ', nextStep: 'Sonraki Adım', diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index 91978d6402..a4d3a2f8f3 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Поле введення користувача', - helpLink: 'Посилання на допомогу', + helpLink: 'Довідковий центр', about: 'Про', createdBy: 'Створено ', nextStep: 'Наступний крок', diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index 9367dae8f9..a5f348f586 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -309,7 +309,7 @@ const translation = { }, panel: { userInputField: 'Trường đầu vào của người dùng', - helpLink: 'Liên kết trợ giúp', + helpLink: 'Trung tâm trợ giúp', about: 'Giới thiệu', createdBy: 'Tạo bởi ', nextStep: 'Bước tiếp theo', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 90a2d915b9..1d2507f969 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -358,7 +358,7 @@ const translation = { panel: { userInputField: '用户输入字段', changeBlock: '更改节点', - helpLink: '帮助链接', + helpLink: '帮助文档', about: '关于', createdBy: '作者', nextStep: '下一步', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index d31e6f3b60..837aaf1831 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -310,7 +310,7 @@ const translation = { panel: { userInputField: '用戶輸入字段', changeBlock: '更改節點', - helpLink: '幫助鏈接', + helpLink: '幫助文件', about: '關於', createdBy: '作者', nextStep: '下一步', From 77e9bae3ff8e298827d3ee0cebcbaf4d0e73ef37 Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Wed, 22 Oct 2025 12:29:14 +0800 Subject: [PATCH 09/11] feat(workflow): polish featured tools recommendations --- .../workflow/block-selector/all-tools.tsx | 54 ++++++++----------- .../workflow/block-selector/tabs.tsx | 25 ++++++++- .../workflow/block-selector/tool-picker.tsx | 23 +++++++- web/i18n/en-US/workflow.ts | 1 + web/i18n/zh-Hans/workflow.ts | 3 +- web/service/use-plugins.ts | 32 ++++++++++- 6 files changed, 101 insertions(+), 37 deletions(-) diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index e071c665ae..e7643dce55 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -25,13 +25,12 @@ import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general' import { useGetLanguage } from '@/context/i18n' import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list' import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list' +import type { Plugin } from '../../plugins/types' import { PluginCategoryEnum } from '../../plugins/types' import { useMarketplacePlugins } from '../../plugins/marketplace/hooks' import { useGlobalPublicStore } from '@/context/global-public-context' import RAGToolSuggestions from './rag-tool-suggestions' import FeaturedTools from './featured-tools' -import { useCheckInstalled, useRecommendedMarketplacePlugins } from '@/service/use-plugins' -import { useInvalidateAllBuiltInTools } from '@/service/use-tools' import Link from 'next/link' type AllToolsProps = { @@ -50,6 +49,12 @@ type AllToolsProps = { canChooseMCPTool?: boolean onTagsChange?: Dispatch> isInRAGPipeline?: boolean + featuredPlugins?: Plugin[] + featuredLoading?: boolean + featuredInstalledPluginIds?: Set + featuredInstallLoading?: boolean + showFeatured?: boolean + onFeaturedInstallSuccess?: () => Promise | void } const DEFAULT_TAGS: AllToolsProps['tags'] = [] @@ -70,6 +75,12 @@ const AllTools = ({ canChooseMCPTool, onTagsChange, isInRAGPipeline = false, + featuredPlugins = [], + featuredLoading = false, + featuredInstalledPluginIds = new Set(), + featuredInstallLoading = false, + showFeatured = false, + onFeaturedInstallSuccess, }: AllToolsProps) => { const { t } = useTranslation() const language = useGetLanguage() @@ -149,26 +160,6 @@ const AllTools = ({ } = useMarketplacePlugins() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) - const { - data: recommendedPlugins = [], - isLoading: isLoadingRecommended, - } = useRecommendedMarketplacePlugins({ - enabled: enable_marketplace && !isInRAGPipeline, - }) - const recommendedPluginIds = useMemo( - () => recommendedPlugins.map(plugin => plugin.plugin_id), - [recommendedPlugins], - ) - const installedCheck = useCheckInstalled({ - pluginIds: recommendedPluginIds, - enabled: recommendedPluginIds.length > 0, - }) - const installedPluginIds = useMemo( - () => new Set(installedCheck.data?.plugins.map(plugin => plugin.plugin_id) ?? []), - [installedCheck.data], - ) - const loadingRecommendedInstallStatus = installedCheck.isLoading || installedCheck.isRefetching - const invalidateBuiltInTools = useInvalidateAllBuiltInTools() useEffect(() => { if (!enable_marketplace) return @@ -189,12 +180,12 @@ const AllTools = ({ const hasToolsContent = tools.length > 0 const hasPluginContent = enable_marketplace && notInstalledPlugins.length > 0 const shouldShowEmptyState = hasFilter && !hasToolsContent && !hasPluginContent - const shouldShowFeatured = enable_marketplace + const shouldShowFeatured = showFeatured + && enable_marketplace + && !isInRAGPipeline && activeTab === ToolTypeEnum.All && !hasFilter - && !isLoadingRecommended - && !isInRAGPipeline - && recommendedPlugins.length > 0 + && (featuredLoading || featuredPlugins.length > 0) return (
@@ -235,17 +226,16 @@ const AllTools = ({ )} {shouldShowFeatured && ( { - invalidateBuiltInTools() - await installedCheck.refetch() + await onFeaturedInstallSuccess?.() }} /> )} diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 871ab3405a..444ddcd419 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -1,6 +1,6 @@ import type { Dispatch, FC, SetStateAction } from 'react' import { memo } from 'react' -import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools' import type { BlockEnum, NodeDefault, @@ -13,6 +13,8 @@ import AllStartBlocks from './all-start-blocks' import AllTools from './all-tools' import DataSources from './data-sources' import cn from '@/utils/classnames' +import { useFeaturedToolsRecommendations } from '@/service/use-plugins' +import { useGlobalPublicStore } from '@/context/global-public-context' export type TabsProps = { activeTab: TabsEnum @@ -53,6 +55,16 @@ const Tabs: FC = ({ const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() const { data: mcpTools } = useAllMCPTools() + const invalidateBuiltInTools = useInvalidateAllBuiltInTools() + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + const inRAGPipeline = dataSources.length > 0 + const { + plugins: featuredPlugins = [], + isLoading: isFeaturedLoading, + installedIds: featuredInstalledIds, + installStatusLoading: featuredInstallLoading, + refetchInstallStatus: refetchFeaturedInstallStatus, + } = useFeaturedToolsRecommendations(enable_marketplace && !inRAGPipeline) return (
e.stopPropagation()}> @@ -127,7 +139,16 @@ const Tabs: FC = ({ mcpTools={mcpTools || []} canChooseMCPTool onTagsChange={onTagsChange} - isInRAGPipeline={dataSources.length > 0} + isInRAGPipeline={inRAGPipeline} + featuredPlugins={featuredPlugins} + featuredLoading={isFeaturedLoading} + featuredInstalledPluginIds={featuredInstalledIds} + featuredInstallLoading={featuredInstallLoading} + showFeatured={enable_marketplace && !inRAGPipeline} + onFeaturedInstallSuccess={async () => { + invalidateBuiltInTools() + await refetchFeaturedInstallStatus() + }} /> ) } diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index ae4b0d4f02..809a7b1cc9 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -23,7 +23,9 @@ import { } from '@/service/tools' import type { CustomCollectionBackend } from '@/app/components/tools/types' import Toast from '@/app/components/base/toast' -import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools, useInvalidateAllCustomTools } from '@/service/use-tools' +import { useFeaturedToolsRecommendations } from '@/service/use-plugins' +import { useGlobalPublicStore } from '@/context/global-public-context' import cn from '@/utils/classnames' type Props = { @@ -61,11 +63,21 @@ const ToolPicker: FC = ({ const [searchText, setSearchText] = useState('') const [tags, setTags] = useState([]) + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const invalidateCustomTools = useInvalidateAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() const { data: mcpTools } = useAllMCPTools() + const invalidateBuiltInTools = useInvalidateAllBuiltInTools() + + const { + plugins: featuredPlugins = [], + isLoading: isFeaturedLoading, + installedIds: featuredInstalledIds, + installStatusLoading: featuredInstallLoading, + refetchInstallStatus: refetchFeaturedInstallStatus, + } = useFeaturedToolsRecommendations(enable_marketplace) const { builtinToolList, customToolList, workflowToolList } = useMemo(() => { if (scope === 'plugins') { @@ -179,6 +191,15 @@ const ToolPicker: FC = ({ selectedTools={selectedTools} canChooseMCPTool={canChooseMCPTool} onTagsChange={setTags} + featuredPlugins={featuredPlugins} + featuredLoading={isFeaturedLoading} + featuredInstalledPluginIds={featuredInstalledIds} + featuredInstallLoading={featuredInstallLoading} + showFeatured={scope === 'all' && enable_marketplace} + onFeaturedInstallSuccess={async () => { + invalidateBuiltInTools() + await refetchFeaturedInstallStatus() + }} />
diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 30ea04d6ae..608e8dc420 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -283,6 +283,7 @@ const translation = { 'pluginByAuthor': 'By {{author}}', 'usePlugin': 'Select tool', 'hideActions': 'Hide tools', + 'noFeaturedPlugins': 'Discover more tools in Marketplace', }, blocks: { 'start': 'User Input', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 1d2507f969..6970c2ee16 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -269,6 +269,7 @@ const translation = { 'pluginByAuthor': '来自 {{author}}', 'usePlugin': '选择工具', 'hideActions': '收起工具', + 'noFeaturedPlugins': '前往插件市场查看更多工具', }, blocks: { 'start': '用户输入', @@ -358,7 +359,7 @@ const translation = { panel: { userInputField: '用户输入字段', changeBlock: '更改节点', - helpLink: '帮助文档', + helpLink: '帮助链接', about: '关于', createdBy: '作者', nextStep: '下一步', diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index fcd626b58e..8eb7cbb0da 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import type { FormOption, ModelProvider, @@ -96,6 +96,36 @@ export const useRecommendedMarketplacePlugins = ({ }) } +export const useFeaturedToolsRecommendations = (enabled: boolean, limit = 15) => { + const { + data: plugins = [], + isLoading, + } = useRecommendedMarketplacePlugins({ + enabled, + limit, + }) + const pluginIds = useMemo( + () => plugins.map(plugin => plugin.plugin_id), + [plugins], + ) + const installedCheck = useCheckInstalled({ + pluginIds, + enabled: enabled && pluginIds.length > 0, + }) + const installedIds = useMemo( + () => new Set(installedCheck.data?.plugins.map(plugin => plugin.plugin_id) ?? []), + [installedCheck.data], + ) + const installStatusLoading = installedCheck.isLoading || installedCheck.isRefetching + return { + plugins, + isLoading, + installedIds, + installStatusLoading, + refetchInstallStatus: installedCheck.refetch, + } +} + export const useInstalledPluginList = (disable?: boolean, pageSize = 100) => { const fetchPlugins = async ({ pageParam = 1 }) => { const response = await get( From e2539e91eb68dc435e7c7275c7f8fd768019b25e Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Wed, 22 Oct 2025 12:41:29 +0800 Subject: [PATCH 10/11] fix: view docs --- web/i18n/en-US/common.ts | 2 +- web/i18n/en-US/workflow.ts | 2 +- web/i18n/ja-JP/common.ts | 2 +- web/i18n/ja-JP/workflow.ts | 2 +- web/i18n/zh-Hans/common.ts | 2 +- web/i18n/zh-Hans/workflow.ts | 2 +- web/i18n/zh-Hant/common.ts | 2 +- web/i18n/zh-Hant/workflow.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index bbed96e3b7..adf2ec98f4 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -181,7 +181,7 @@ const translation = { emailSupport: 'Email Support', workspace: 'Workspace', createWorkspace: 'Create Workspace', - helpCenter: 'Docs', + helpCenter: 'View Docs', support: 'Support', compliance: 'Compliance', communityFeedback: 'Feedback', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 608e8dc420..6f5cb1d355 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -373,7 +373,7 @@ const translation = { panel: { userInputField: 'User Input Field', changeBlock: 'Change Node', - helpLink: 'Docs', + helpLink: 'View Docs', about: 'About', createdBy: 'Created By ', nextStep: 'Next Step', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index 60e95ef10d..21abb3f5fc 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -171,7 +171,7 @@ const translation = { emailSupport: 'サポート', workspace: 'ワークスペース', createWorkspace: 'ワークスペースを作成', - helpCenter: 'ヘルプ', + helpCenter: 'ドキュメントを見る', support: 'サポート', compliance: 'コンプライアンス', communityFeedback: 'フィードバック', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 5dfb3d983a..444242c2c7 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -349,7 +349,7 @@ const translation = { panel: { userInputField: 'ユーザー入力欄', changeBlock: 'ノード変更', - helpLink: 'ヘルプ', + helpLink: 'ドキュメントを見る', about: '詳細', createdBy: '作成者', nextStep: '次のステップ', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index 6175a36bc2..d46f4a813a 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -181,7 +181,7 @@ const translation = { emailSupport: '邮件支持', workspace: '工作空间', createWorkspace: '创建工作空间', - helpCenter: '帮助文档', + helpCenter: '查看帮助文档', support: '支持', compliance: '合规', communityFeedback: '用户反馈', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 6970c2ee16..416c9da6ae 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -359,7 +359,7 @@ const translation = { panel: { userInputField: '用户输入字段', changeBlock: '更改节点', - helpLink: '帮助链接', + helpLink: '查看帮助文档', about: '关于', createdBy: '作者', nextStep: '下一步', diff --git a/web/i18n/zh-Hant/common.ts b/web/i18n/zh-Hant/common.ts index 273ecb010f..89933ba968 100644 --- a/web/i18n/zh-Hant/common.ts +++ b/web/i18n/zh-Hant/common.ts @@ -160,7 +160,7 @@ const translation = { emailSupport: '電子郵件支援', workspace: '工作空間', createWorkspace: '建立工作空間', - helpCenter: '幫助文件', + helpCenter: '查看幫助文件', communityFeedback: '使用者反饋', roadmap: '路線圖', community: '社群', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 837aaf1831..d855911719 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -310,7 +310,7 @@ const translation = { panel: { userInputField: '用戶輸入字段', changeBlock: '更改節點', - helpLink: '幫助文件', + helpLink: '查看幫助文件', about: '關於', createdBy: '作者', nextStep: '下一步', From 94ea289c7516097b45474815b457d554a216d854 Mon Sep 17 00:00:00 2001 From: lyzno1 Date: Wed, 22 Oct 2025 12:46:07 +0800 Subject: [PATCH 11/11] fix: suggestions tools list --- .../workflow/block-selector/all-tools.tsx | 6 - .../block-selector/featured-tools.tsx | 283 ++++++------------ .../workflow/block-selector/tabs.tsx | 6 - .../workflow/block-selector/tool-picker.tsx | 6 - web/service/use-plugins.ts | 19 +- 5 files changed, 93 insertions(+), 227 deletions(-) diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index e7643dce55..d473ddb541 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -51,8 +51,6 @@ type AllToolsProps = { isInRAGPipeline?: boolean featuredPlugins?: Plugin[] featuredLoading?: boolean - featuredInstalledPluginIds?: Set - featuredInstallLoading?: boolean showFeatured?: boolean onFeaturedInstallSuccess?: () => Promise | void } @@ -77,8 +75,6 @@ const AllTools = ({ isInRAGPipeline = false, featuredPlugins = [], featuredLoading = false, - featuredInstalledPluginIds = new Set(), - featuredInstallLoading = false, showFeatured = false, onFeaturedInstallSuccess, }: AllToolsProps) => { @@ -231,8 +227,6 @@ const AllTools = ({ onSelect={onSelect} selectedTools={selectedTools} canChooseMCPTool={canChooseMCPTool} - installedPluginIds={featuredInstalledPluginIds} - loadingInstalledStatus={featuredInstallLoading} isLoading={featuredLoading} onInstallSuccess={async () => { await onFeaturedInstallSuccess?.() diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 1ae3d9fe22..e65b99f21c 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -6,15 +6,15 @@ import type { ToolDefaultValue, ToolValue } from './types' import type { Plugin } from '@/app/components/plugins/types' import { useGetLanguage } from '@/context/i18n' import Button from '@/app/components/base/button' -import ActionItem from './tool/action-item' -import type { Tool } from '@/app/components/tools/types' -import { CollectionType } from '@/app/components/tools/types' import BlockIcon from '../block-icon' -import { RiArrowDownSLine, RiArrowRightSLine, RiArrowUpSLine, RiLoader2Line } from '@remixicon/react' +import { RiArrowDownSLine, RiArrowRightSLine, RiLoader2Line, RiMoreLine } from '@remixicon/react' import { useInstallPackageFromMarketPlace } from '@/service/use-plugins' import Loading from '@/app/components/base/loading' import Link from 'next/link' import { getMarketplaceUrl } from '@/utils/var' +import { ToolTypeEnum } from './types' +import { ViewType } from './view-type-select' +import Tools from './tools' const MAX_RECOMMENDED_COUNT = 15 const INITIAL_VISIBLE_COUNT = 5 @@ -25,18 +25,10 @@ type FeaturedToolsProps = { onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void selectedTools?: ToolValue[] canChooseMCPTool?: boolean - installedPluginIds: Set - loadingInstalledStatus: boolean isLoading?: boolean onInstallSuccess?: () => void } -function isToolSelected(tool: Tool, provider: ToolWithProvider, selectedTools?: ToolValue[]): boolean { - if (!selectedTools || !selectedTools.length) - return false - return selectedTools.some(item => (item.provider_name === provider.name || item.provider_name === provider.id) && item.tool_name === tool.name) -} - const STORAGE_KEY = 'workflow_tools_featured_collapsed' const FeaturedTools = ({ @@ -45,8 +37,6 @@ const FeaturedTools = ({ onSelect, selectedTools, canChooseMCPTool, - installedPluginIds, - loadingInstalledStatus, isLoading = false, onInstallSuccess, }: FeaturedToolsProps) => { @@ -88,9 +78,22 @@ const FeaturedTools = ({ [plugins, visibleCount], ) + const installedProviders = useMemo( + () => + visiblePlugins + .map(plugin => providerMap.get(plugin.plugin_id)) + .filter((provider): provider is ToolWithProvider => Boolean(provider)), + [visiblePlugins, providerMap], + ) + + const uninstalledPlugins = useMemo( + () => visiblePlugins.filter(plugin => !providerMap.has(plugin.plugin_id)), + [visiblePlugins, providerMap], + ) + const showMore = visibleCount < Math.min(MAX_RECOMMENDED_COUNT, plugins.length) const isMutating = installMutation.isPending - const showEmptyState = !isLoading && !visiblePlugins.length + const showEmptyState = !isLoading && visiblePlugins.length === 0 return (
@@ -119,36 +122,58 @@ const FeaturedTools = ({

)} - {!isLoading && visiblePlugins.length > 0 && ( -
- {visiblePlugins.map(plugin => renderFeaturedToolItem({ - plugin, - providerMap, - installedPluginIds, - installMutationPending: isMutating, - installingIdentifier, - loadingInstalledStatus, - canChooseMCPTool, - onSelect, - selectedTools, - language, - installPlugin: installMutation.mutate, - setInstallingIdentifier, - }))} -
+ {!showEmptyState && !isLoading && ( + <> + {installedProviders.length > 0 && ( + + )} + + {uninstalledPlugins.length > 0 && ( +
+ {uninstalledPlugins.map(plugin => ( + { + if (isMutating) + return + setInstallingIdentifier(plugin.latest_package_identifier) + installMutation.mutate(plugin.latest_package_identifier) + }} + t={t} + /> + ))} +
+ )} + )} {!isLoading && visiblePlugins.length > 0 && showMore && ( - +
+ +
+
+ {t('common.operation.more')} +
+
)} )} @@ -156,177 +181,51 @@ const FeaturedTools = ({ ) } -type FeaturedToolItemProps = { +type FeaturedToolUninstalledItemProps = { plugin: Plugin - provider: ToolWithProvider | undefined - isInstalled: boolean - installDisabled: boolean - canChooseMCPTool?: boolean - onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void - selectedTools?: ToolValue[] language: string + installing: boolean onInstall: () => void - isInstalling: boolean + t: (key: string, options?: Record) => string } -function FeaturedToolItem({ +function FeaturedToolUninstalledItem({ plugin, - provider, - isInstalled, - installDisabled, - canChooseMCPTool, - onSelect, - selectedTools, language, + installing, onInstall, - isInstalling, -}: FeaturedToolItemProps) { - const { t } = useTranslation() - const [isExpanded, setExpanded] = useState(false) - const hasProvider = Boolean(provider) - const installCountLabel = t('plugin.install', { num: plugin.install_count?.toLocaleString() ?? 0 }) + t, +}: FeaturedToolUninstalledItemProps) { + const label = plugin.label?.[language] || plugin.name const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief - - useEffect(() => { - if (!hasProvider) - setExpanded(false) - }, [hasProvider]) - - let toggleLabel: string - if (!hasProvider) - toggleLabel = t('workflow.common.syncingData') - else if (isExpanded) - toggleLabel = t('workflow.tabs.hideActions') - else - toggleLabel = t('workflow.tabs.usePlugin') + const installCountLabel = t('plugin.install', { num: plugin.install_count?.toLocaleString() ?? 0 }) return ( -
-
+
+
-
-
-
- {plugin.label?.[language] || plugin.name} -
- {isInstalled && ( - - {t('workflow.tabs.installed')} - - )} -
-
- {description} -
-
- {installCountLabel} - {plugin.org && {t('workflow.tabs.pluginByAuthor', { author: plugin.org })}} -
-
-
- {!isInstalled && ( - - )} - {isInstalled && ( - +
+
{label}
+ {description && ( +
{description}
)}
- {isInstalled && hasProvider && isExpanded && ( -
- {provider.tools.map((tool) => { - const isSelected = isToolSelected(tool, provider, selectedTools) - const isMCPTool = provider.type === CollectionType.mcp - const disabled = isSelected || (!canChooseMCPTool && isMCPTool) - - return ( - - ) - })} -
- )} +
+ {installCountLabel} + +
) } -type RenderFeaturedToolParams = { - plugin: Plugin - providerMap: Map - installedPluginIds: Set - installMutationPending: boolean - installingIdentifier: string | null - loadingInstalledStatus: boolean - canChooseMCPTool?: boolean - onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void - selectedTools?: ToolValue[] - language: string - installPlugin: (uniqueIdentifier: string) => void - setInstallingIdentifier: (identifier: string | null) => void -} - -function renderFeaturedToolItem({ - plugin, - providerMap, - installedPluginIds, - installMutationPending, - installingIdentifier, - loadingInstalledStatus, - canChooseMCPTool, - onSelect, - selectedTools, - language, - installPlugin, - setInstallingIdentifier, -}: RenderFeaturedToolParams) { - const provider = providerMap.get(plugin.plugin_id) - const isInstalled = installedPluginIds.has(plugin.plugin_id) - const isInstalling = installMutationPending && installingIdentifier === plugin.latest_package_identifier - - return ( - { - if (installMutationPending) - return - setInstallingIdentifier(plugin.latest_package_identifier) - installPlugin(plugin.latest_package_identifier) - }} - isInstalling={isInstalling} - /> - ) -} - export default FeaturedTools diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 444ddcd419..17de4b74d2 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -61,9 +61,6 @@ const Tabs: FC = ({ const { plugins: featuredPlugins = [], isLoading: isFeaturedLoading, - installedIds: featuredInstalledIds, - installStatusLoading: featuredInstallLoading, - refetchInstallStatus: refetchFeaturedInstallStatus, } = useFeaturedToolsRecommendations(enable_marketplace && !inRAGPipeline) return ( @@ -142,12 +139,9 @@ const Tabs: FC = ({ isInRAGPipeline={inRAGPipeline} featuredPlugins={featuredPlugins} featuredLoading={isFeaturedLoading} - featuredInstalledPluginIds={featuredInstalledIds} - featuredInstallLoading={featuredInstallLoading} showFeatured={enable_marketplace && !inRAGPipeline} onFeaturedInstallSuccess={async () => { invalidateBuiltInTools() - await refetchFeaturedInstallStatus() }} /> ) diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index 809a7b1cc9..c438e45042 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -74,9 +74,6 @@ const ToolPicker: FC = ({ const { plugins: featuredPlugins = [], isLoading: isFeaturedLoading, - installedIds: featuredInstalledIds, - installStatusLoading: featuredInstallLoading, - refetchInstallStatus: refetchFeaturedInstallStatus, } = useFeaturedToolsRecommendations(enable_marketplace) const { builtinToolList, customToolList, workflowToolList } = useMemo(() => { @@ -193,12 +190,9 @@ const ToolPicker: FC = ({ onTagsChange={setTags} featuredPlugins={featuredPlugins} featuredLoading={isFeaturedLoading} - featuredInstalledPluginIds={featuredInstalledIds} - featuredInstallLoading={featuredInstallLoading} showFeatured={scope === 'all' && enable_marketplace} onFeaturedInstallSuccess={async () => { invalidateBuiltInTools() - await refetchFeaturedInstallStatus() }} />
diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 8eb7cbb0da..5904da6d27 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo } from 'react' +import { useCallback, useEffect } from 'react' import type { FormOption, ModelProvider, @@ -104,25 +104,10 @@ export const useFeaturedToolsRecommendations = (enabled: boolean, limit = 15) => enabled, limit, }) - const pluginIds = useMemo( - () => plugins.map(plugin => plugin.plugin_id), - [plugins], - ) - const installedCheck = useCheckInstalled({ - pluginIds, - enabled: enabled && pluginIds.length > 0, - }) - const installedIds = useMemo( - () => new Set(installedCheck.data?.plugins.map(plugin => plugin.plugin_id) ?? []), - [installedCheck.data], - ) - const installStatusLoading = installedCheck.isLoading || installedCheck.isRefetching + return { plugins, isLoading, - installedIds, - installStatusLoading, - refetchInstallStatus: installedCheck.refetch, } }