merge hitl

This commit is contained in:
JzoNg
2026-05-13 14:55:44 +08:00
53 changed files with 2321 additions and 1808 deletions

View File

@@ -253,7 +253,20 @@ class AdvancedChatAppGenerator(MessageBasedAppGenerator):
):
"""
Resume a paused advanced chat execution.
``trace_manager`` is transient and excluded from generate-entity serialization,
so resumed executions rebuild it here before persistence layers receive the entity.
"""
if application_generate_entity.trace_manager is None:
application_generate_entity = application_generate_entity.model_copy(
update={
"trace_manager": TraceQueueManager(
app_id=app_model.id,
user_id=user.id if isinstance(user, Account) else user.session_id,
)
}
)
return self._generate(
workflow=workflow,
user=user,

View File

@@ -272,7 +272,20 @@ class WorkflowAppGenerator(BaseAppGenerator):
) -> Mapping[str, Any] | Generator[str | Mapping[str, Any], None, None]:
"""
Resume a paused workflow execution using the persisted runtime state.
``trace_manager`` is transient and excluded from generate-entity serialization,
so resumed executions rebuild it here before persistence layers receive the entity.
"""
if application_generate_entity.trace_manager is None:
application_generate_entity = application_generate_entity.model_copy(
update={
"trace_manager": TraceQueueManager(
app_id=app_model.id,
user_id=user.id if isinstance(user, Account) else user.session_id,
)
}
)
return self._generate(
app_model=app_model,
workflow=workflow,

View File

@@ -197,6 +197,7 @@ class TestAdvancedChatAppGeneratorInternals:
def test_resume_delegates_to_generate(self, monkeypatch: pytest.MonkeyPatch):
generator = AdvancedChatAppGenerator()
existing_trace_manager = SimpleNamespace(app_id="existing-app", user_id="existing-user")
application_generate_entity = AdvancedChatAppGenerateEntity.model_construct(
task_id="task",
app_config=self._build_app_config(),
@@ -207,22 +208,25 @@ class TestAdvancedChatAppGeneratorInternals:
stream=True,
invoke_from=InvokeFrom.WEB_APP,
extras={},
trace_manager=None,
trace_manager=existing_trace_manager,
workflow_run_id="run-id",
)
captured: dict[str, object] = {}
captured_entity: AdvancedChatAppGenerateEntity | None = None
captured_graph_runtime_state: object | None = None
def _fake_generate(**kwargs):
captured.update(kwargs)
return {"resumed": True}
nonlocal captured_entity, captured_graph_runtime_state
captured_entity = kwargs["application_generate_entity"]
captured_graph_runtime_state = kwargs["graph_runtime_state"]
return SimpleNamespace(resumed=True)
monkeypatch.setattr(generator, "_generate", _fake_generate)
result = generator.resume(
app_model=SimpleNamespace(),
app_model=SimpleNamespace(id="app-id"),
workflow=SimpleNamespace(),
user=SimpleNamespace(),
user=SimpleNamespace(id="end-user-id", session_id="session-id"),
conversation=SimpleNamespace(id="conversation-id"),
message=SimpleNamespace(id="message-id"),
application_generate_entity=application_generate_entity,
@@ -232,8 +236,10 @@ class TestAdvancedChatAppGeneratorInternals:
pause_state_config=None,
)
assert result == {"resumed": True}
assert captured["graph_runtime_state"] is not None
assert result.resumed is True
assert captured_entity is not None
assert captured_entity.trace_manager is existing_trace_manager
assert captured_graph_runtime_state is not None
def test_single_iteration_generate_builds_debug_task(self, monkeypatch: pytest.MonkeyPatch):
generator = AdvancedChatAppGenerator()
@@ -1243,3 +1249,119 @@ class TestAdvancedChatAppGeneratorInternals:
)
assert captured["application_generate_entity"].parent_message_id == UUID_NIL
class TestAdvancedChatAppGeneratorResume:
@staticmethod
def _build_app_config() -> WorkflowUIBasedAppConfig:
return WorkflowUIBasedAppConfig(
tenant_id="tenant",
app_id="app",
app_mode=AppMode.ADVANCED_CHAT,
additional_features=AppAdditionalFeatures(),
variables=[],
workflow_id="workflow-id",
)
def test_resume_restores_trace_manager_when_missing(self, monkeypatch: pytest.MonkeyPatch):
generator = AdvancedChatAppGenerator()
application_generate_entity = AdvancedChatAppGenerateEntity.model_construct(
task_id="task",
app_config=self._build_app_config(),
file_upload_config=None,
conversation_id="conversation-id",
inputs={},
query="hello",
files=[],
parent_message_id="parent-message-id",
user_id="user",
stream=False,
invoke_from=InvokeFrom.WEB_APP,
extras={},
trace_manager=None,
workflow_run_id="run-id",
)
DummyTraceQueueManager = type(
"_DummyTraceQueueManager",
(TraceQueueManager,),
{
"__init__": lambda self, app_id=None, user_id=None: (
setattr(self, "app_id", app_id) or setattr(self, "user_id", user_id)
)
},
)
monkeypatch.setattr(
"core.app.apps.advanced_chat.app_generator.TraceQueueManager",
DummyTraceQueueManager,
)
captured_entity: AdvancedChatAppGenerateEntity | None = None
def _fake_generate(**kwargs):
nonlocal captured_entity
captured_entity = kwargs["application_generate_entity"]
return SimpleNamespace(ok=True)
monkeypatch.setattr(generator, "_generate", _fake_generate)
result = generator.resume(
app_model=SimpleNamespace(id="app-id"),
workflow=SimpleNamespace(),
user=SimpleNamespace(id="end-user-id", session_id="session-id"),
conversation=SimpleNamespace(id="conversation-id"),
message=SimpleNamespace(id="message-id"),
application_generate_entity=application_generate_entity,
workflow_execution_repository=SimpleNamespace(),
workflow_node_execution_repository=SimpleNamespace(),
graph_runtime_state=SimpleNamespace(),
)
assert result.ok is True
assert captured_entity is not None
trace_manager = captured_entity.trace_manager
assert isinstance(trace_manager, DummyTraceQueueManager)
assert trace_manager.app_id == "app-id"
assert trace_manager.user_id == "session-id"
def test_resume_preserves_existing_trace_manager(self, monkeypatch: pytest.MonkeyPatch):
generator = AdvancedChatAppGenerator()
existing_trace_manager = SimpleNamespace(app_id="existing-app", user_id="existing-user")
application_generate_entity = AdvancedChatAppGenerateEntity.model_construct(
task_id="task",
app_config=self._build_app_config(),
file_upload_config=None,
conversation_id="conversation-id",
inputs={},
query="hello",
files=[],
parent_message_id="parent-message-id",
user_id="user",
stream=False,
invoke_from=InvokeFrom.WEB_APP,
extras={},
trace_manager=existing_trace_manager,
workflow_run_id="run-id",
)
captured_entity: AdvancedChatAppGenerateEntity | None = None
def _fake_generate(**kwargs):
nonlocal captured_entity
captured_entity = kwargs["application_generate_entity"]
return SimpleNamespace(ok=True)
monkeypatch.setattr(generator, "_generate", _fake_generate)
result = generator.resume(
app_model=SimpleNamespace(id="app-id"),
workflow=SimpleNamespace(),
user=SimpleNamespace(id="end-user-id", session_id="session-id"),
conversation=SimpleNamespace(id="conversation-id"),
message=SimpleNamespace(id="message-id"),
application_generate_entity=application_generate_entity,
workflow_execution_repository=SimpleNamespace(),
workflow_node_execution_repository=SimpleNamespace(),
graph_runtime_state=SimpleNamespace(),
)
assert result.ok is True
assert captured_entity is not None
assert captured_entity.trace_manager is existing_trace_manager

View File

@@ -228,7 +228,11 @@ def test_workflow_app_pause_resume_matches_baseline(mocker: MockerFixture):
app_model=SimpleNamespace(mode="workflow"),
workflow=SimpleNamespace(),
user=SimpleNamespace(),
application_generate_entity=SimpleNamespace(stream=False, invoke_from=InvokeFrom.SERVICE_API),
application_generate_entity=SimpleNamespace(
stream=False,
invoke_from=InvokeFrom.SERVICE_API,
trace_manager=SimpleNamespace(),
),
graph_runtime_state=resumed_state,
workflow_execution_repository=SimpleNamespace(),
workflow_node_execution_repository=SimpleNamespace(),
@@ -270,7 +274,11 @@ def test_advanced_chat_pause_resume_matches_baseline(mocker: MockerFixture):
user=SimpleNamespace(),
conversation=SimpleNamespace(id="conv"),
message=SimpleNamespace(id="msg"),
application_generate_entity=SimpleNamespace(stream=False, invoke_from=InvokeFrom.SERVICE_API),
application_generate_entity=SimpleNamespace(
stream=False,
invoke_from=InvokeFrom.SERVICE_API,
trace_manager=SimpleNamespace(),
),
workflow_execution_repository=SimpleNamespace(),
workflow_node_execution_repository=SimpleNamespace(),
graph_runtime_state=resumed_state,

View File

@@ -99,7 +99,7 @@ def test_resume_delegates_to_generate(mocker: MockerFixture):
generator = WorkflowAppGenerator()
mock_generate = mocker.patch.object(generator, "_generate", return_value="ok")
application_generate_entity = SimpleNamespace(stream=False, invoke_from="debugger")
application_generate_entity = SimpleNamespace(stream=False, invoke_from="debugger", trace_manager=MagicMock())
runtime_state = MagicMock(name="runtime-state")
pause_config = MagicMock(name="pause-config")

View File

@@ -186,3 +186,114 @@ class TestWorkflowAppGeneratorGenerate:
)
assert result == {"ok": True}
class TestWorkflowAppGeneratorResume:
def test_resume_restores_trace_manager_when_missing(self, monkeypatch: pytest.MonkeyPatch):
generator = WorkflowAppGenerator()
app_config = WorkflowUIBasedAppConfig(
tenant_id="tenant",
app_id="app",
app_mode=AppMode.WORKFLOW,
additional_features=AppAdditionalFeatures(),
variables=[],
workflow_id="workflow-id",
)
application_generate_entity = WorkflowAppGenerateEntity.model_construct(
task_id="task",
app_config=app_config,
inputs={},
files=[],
user_id="user",
stream=False,
invoke_from=InvokeFrom.WEB_APP,
extras={},
trace_manager=None,
workflow_execution_id="run-id",
call_depth=0,
)
DummyTraceQueueManager = type(
"_DummyTraceQueueManager",
(TraceQueueManager,),
{
"__init__": lambda self, app_id=None, user_id=None: (
setattr(self, "app_id", app_id) or setattr(self, "user_id", user_id)
)
},
)
monkeypatch.setattr(
"core.app.apps.workflow.app_generator.TraceQueueManager",
DummyTraceQueueManager,
)
captured_entity: WorkflowAppGenerateEntity | None = None
def _fake_generate(**kwargs):
nonlocal captured_entity
captured_entity = kwargs["application_generate_entity"]
return SimpleNamespace(ok=True)
monkeypatch.setattr(generator, "_generate", _fake_generate)
result = generator.resume(
app_model=SimpleNamespace(id="app-id"),
workflow=SimpleNamespace(),
user=SimpleNamespace(id="end-user-id", session_id="session-id"),
application_generate_entity=application_generate_entity,
graph_runtime_state=SimpleNamespace(),
workflow_execution_repository=SimpleNamespace(),
workflow_node_execution_repository=SimpleNamespace(),
)
assert result.ok is True
assert captured_entity is not None
trace_manager = captured_entity.trace_manager
assert isinstance(trace_manager, DummyTraceQueueManager)
assert trace_manager.app_id == "app-id"
assert trace_manager.user_id == "session-id"
def test_resume_preserves_existing_trace_manager(self, monkeypatch: pytest.MonkeyPatch):
generator = WorkflowAppGenerator()
app_config = WorkflowUIBasedAppConfig(
tenant_id="tenant",
app_id="app",
app_mode=AppMode.WORKFLOW,
additional_features=AppAdditionalFeatures(),
variables=[],
workflow_id="workflow-id",
)
existing_trace_manager = SimpleNamespace(app_id="existing-app", user_id="existing-user")
application_generate_entity = WorkflowAppGenerateEntity.model_construct(
task_id="task",
app_config=app_config,
inputs={},
files=[],
user_id="user",
stream=False,
invoke_from=InvokeFrom.WEB_APP,
extras={},
trace_manager=existing_trace_manager,
workflow_execution_id="run-id",
call_depth=0,
)
captured_entity: WorkflowAppGenerateEntity | None = None
def _fake_generate(**kwargs):
nonlocal captured_entity
captured_entity = kwargs["application_generate_entity"]
return SimpleNamespace(ok=True)
monkeypatch.setattr(generator, "_generate", _fake_generate)
result = generator.resume(
app_model=SimpleNamespace(id="app-id"),
workflow=SimpleNamespace(),
user=SimpleNamespace(id="end-user-id", session_id="session-id"),
application_generate_entity=application_generate_entity,
graph_runtime_state=SimpleNamespace(),
workflow_execution_repository=SimpleNamespace(),
workflow_node_execution_repository=SimpleNamespace(),
)
assert result.ok is True
assert captured_entity is not None
assert captured_entity.trace_manager is existing_trace_manager

View File

@@ -120,6 +120,9 @@
}
},
"web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx": {
"react/static-components": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
@@ -192,6 +195,11 @@
"count": 1
}
},
"web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/app/annotation/batch-add-annotation-modal/index.tsx": {
"erasable-syntax-only/enums": {
"count": 1
@@ -411,7 +419,7 @@
},
"web/app/components/app/configuration/debug/index.tsx": {
"react/set-state-in-effect": {
"count": 2
"count": 1
},
"ts/no-explicit-any": {
"count": 11
@@ -472,6 +480,11 @@
"count": 2
}
},
"web/app/components/app/overview/app-chart.tsx": {
"react/component-hook-factories": {
"count": 1
}
},
"web/app/components/app/overview/trigger-card.tsx": {
"ts/no-explicit-any": {
"count": 1
@@ -612,14 +625,14 @@
"react-refresh/only-export-components": {
"count": 1
},
"react/component-hook-factories": {
"count": 1
},
"react/no-nested-component-definitions": {
"count": 1
},
"react/set-state-in-effect": {
"count": 1
},
"react/static-components": {
"count": 2
}
},
"web/app/components/base/carousel/index.tsx": {
@@ -690,9 +703,6 @@
}
},
"web/app/components/base/chat/chat/answer/index.tsx": {
"react/set-state-in-effect": {
"count": 3
},
"ts/no-explicit-any": {
"count": 1
}
@@ -811,7 +821,7 @@
"react-refresh/only-export-components": {
"count": 3
},
"react/component-hook-factories": {
"react/jsx-no-key-after-spread": {
"count": 1
},
"ts/no-explicit-any": {
@@ -929,14 +939,21 @@
"count": 1
}
},
"web/app/components/base/form/form-scenarios/base/__tests__/field.spec.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/base/form/form-scenarios/base/field.tsx": {
"react/component-hook-factories": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/base/form/form-scenarios/base/index.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/base/form/form-scenarios/base/types.ts": {
"erasable-syntax-only/enums": {
"count": 1
@@ -950,10 +967,12 @@
"count": 2
}
},
"web/app/components/base/form/form-scenarios/input-field/__tests__/field.spec.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/base/form/form-scenarios/input-field/field.tsx": {
"react/component-hook-factories": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
@@ -966,10 +985,12 @@
"count": 2
}
},
"web/app/components/base/form/form-scenarios/node-panel/__tests__/field.spec.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/base/form/form-scenarios/node-panel/field.tsx": {
"react/component-hook-factories": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
@@ -1322,9 +1343,6 @@
}
},
"web/app/components/base/markdown-blocks/code-block.tsx": {
"react/set-state-in-effect": {
"count": 7
},
"ts/no-explicit-any": {
"count": 9
}
@@ -1379,7 +1397,7 @@
},
"web/app/components/base/mermaid/index.tsx": {
"react/set-state-in-effect": {
"count": 7
"count": 4
},
"regexp/no-super-linear-backtracking": {
"count": 3
@@ -1659,11 +1677,6 @@
"count": 1
}
},
"web/app/components/base/video-gallery/VideoPlayer.tsx": {
"react/set-state-in-effect": {
"count": 1
}
},
"web/app/components/base/voice-input/__tests__/index.spec.tsx": {
"ts/no-explicit-any": {
"count": 3
@@ -1685,15 +1698,9 @@
"web/app/components/base/with-input-validation/index.stories.tsx": {
"no-console": {
"count": 1
},
"react/component-hook-factories": {
"count": 1
}
},
"web/app/components/base/with-input-validation/index.tsx": {
"react/component-hook-factories": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
@@ -1726,6 +1733,11 @@
"count": 1
}
},
"web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/billing/pricing/types.ts": {
"erasable-syntax-only/enums": {
"count": 1
@@ -1736,6 +1748,11 @@
"count": 4
}
},
"web/app/components/datasets/common/document-status-with-action/status-with-action.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/datasets/common/image-previewer/index.tsx": {
"no-irregular-whitespace": {
"count": 1
@@ -1919,6 +1936,9 @@
}
},
"web/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl/base/options/index.tsx": {
"react/static-components": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
@@ -1934,6 +1954,9 @@
}
},
"web/app/components/datasets/documents/create-from-pipeline/process-documents/form.tsx": {
"react/static-components": {
"count": 2
},
"ts/no-explicit-any": {
"count": 3
}
@@ -1953,6 +1976,11 @@
"count": 1
}
},
"web/app/components/datasets/documents/detail/batch-modal/csv-downloader.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/datasets/documents/detail/completed/common/chunk-content.tsx": {
"react/set-state-in-effect": {
"count": 1
@@ -1979,6 +2007,11 @@
"count": 1
}
},
"web/app/components/datasets/documents/detail/completed/segment-list.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/datasets/documents/detail/context.ts": {
"ts/no-explicit-any": {
"count": 1
@@ -2075,6 +2108,16 @@
"count": 2
}
},
"web/app/components/datasets/metadata/metadata-dataset/dataset-metadata-drawer.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/datasets/metadata/metadata-dataset/select-metadata.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/datasets/metadata/types.ts": {
"erasable-syntax-only/enums": {
"count": 2
@@ -2090,6 +2133,11 @@
"count": 7
}
},
"web/app/components/develop/doc.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/develop/md.tsx": {
"ts/no-empty-object-type": {
"count": 1
@@ -2103,7 +2151,7 @@
"count": 1
},
"react/set-state-in-effect": {
"count": 3
"count": 1
}
},
"web/app/components/explore/banner/indicator-button.tsx": {
@@ -2329,7 +2377,7 @@
},
"web/app/components/header/account-setting/model-provider-page/provider-added-card/cooldown-timer.tsx": {
"react/set-state-in-effect": {
"count": 2
"count": 1
}
},
"web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/use-activate-credential.spec.tsx": {
@@ -2618,9 +2666,19 @@
"count": 1
}
},
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/hidden-fields.spec.tsx": {
"react/static-components": {
"count": 4
}
},
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/show-all-settings.spec.tsx": {
"react/static-components": {
"count": 4
}
},
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/hidden-fields.tsx": {
"react/component-hook-factories": {
"count": 1
"react/static-components": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
@@ -2631,18 +2689,20 @@
"count": 2
}
},
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/index.tsx": {
"react/static-components": {
"count": 6
}
},
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/initial-fields.tsx": {
"react/component-hook-factories": {
"count": 1
"react/static-components": {
"count": 2
},
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/show-all-settings.tsx": {
"react/component-hook-factories": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
@@ -2657,12 +2717,20 @@
"count": 1
}
},
"web/app/components/rag-pipeline/components/panel/input-field/preview/form.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/index.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx": {
"react/static-components": {
"count": 2
},
"ts/no-explicit-any": {
"count": 2
}
@@ -2772,11 +2840,24 @@
"count": 1
}
},
"web/app/components/share/text-generation/run-batch/csv-download/index.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/share/text-generation/run-batch/csv-reader/index.tsx": {
"react/static-components": {
"count": 2
},
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/share/text-generation/run-batch/res-download/index.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/share/text-generation/run-once/index.tsx": {
"react/set-state-in-effect": {
"count": 1
@@ -2846,6 +2927,11 @@
"count": 4
}
},
"web/app/components/workflow-app/components/__tests__/workflow-children.spec.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/workflow-app/components/workflow-children.tsx": {
"ts/no-explicit-any": {
"count": 3
@@ -2901,22 +2987,6 @@
"count": 1
}
},
"web/app/components/workflow/block-selector/featured-tools.tsx": {
"react/set-state-in-effect": {
"count": 2
},
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/workflow/block-selector/featured-triggers.tsx": {
"react/set-state-in-effect": {
"count": 2
},
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/workflow/block-selector/hooks.ts": {
"react/set-state-in-effect": {
"count": 1
@@ -2952,29 +3022,6 @@
"count": 1
}
},
"web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/workflow/block-selector/tool/tool.tsx": {
"react/set-state-in-effect": {
"count": 2
}
},
"web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/workflow/block-selector/trigger-plugin/item.tsx": {
"react/set-state-in-effect": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
},
"web/app/components/workflow/block-selector/types.ts": {
"erasable-syntax-only/enums": {
"count": 4
@@ -3013,6 +3060,11 @@
"count": 1
}
},
"web/app/components/workflow/header/__tests__/index.spec.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/workflow/header/run-mode.tsx": {
"no-console": {
"count": 1
@@ -3159,9 +3211,6 @@
}
},
"web/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars.tsx": {
"react/set-state-in-effect": {
"count": 1
},
"ts/no-explicit-any": {
"count": 6
}
@@ -3201,6 +3250,9 @@
}
},
"web/app/components/workflow/nodes/_base/components/input-var-type-icon.tsx": {
"react/static-components": {
"count": 2
},
"ts/no-explicit-any": {
"count": 1
}
@@ -3273,9 +3325,17 @@
"count": 1
}
},
"web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-icon.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/workflow/nodes/_base/components/variable/variable-label/hooks.ts": {
"react/no-unnecessary-use-prefix": {
"count": 2
},
"react/use-memo": {
"count": 2
}
},
"web/app/components/workflow/nodes/_base/components/variable/variable-label/index.tsx": {
@@ -3285,7 +3345,7 @@
},
"web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx": {
"react/set-state-in-effect": {
"count": 3
"count": 2
},
"ts/no-explicit-any": {
"count": 6
@@ -3363,6 +3423,9 @@
}
},
"web/app/components/workflow/nodes/agent/node.tsx": {
"react/jsx-no-key-after-spread": {
"count": 2
},
"ts/no-explicit-any": {
"count": 2
}
@@ -3554,9 +3617,6 @@
}
},
"web/app/components/workflow/nodes/human-input/components/form-content.tsx": {
"react/component-hook-factories": {
"count": 1
},
"react/no-nested-component-definitions": {
"count": 1
},
@@ -3597,6 +3657,11 @@
"count": 5
}
},
"web/app/components/workflow/nodes/index.tsx": {
"react/static-components": {
"count": 4
}
},
"web/app/components/workflow/nodes/iteration/default.ts": {
"ts/no-explicit-any": {
"count": 1
@@ -3716,11 +3781,6 @@
"count": 2
}
},
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/auto-width-input.tsx": {
"react/set-state-in-effect": {
"count": 1
}
},
"web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts": {
"ts/no-explicit-any": {
"count": 1
@@ -3838,11 +3898,6 @@
"count": 9
}
},
"web/app/components/workflow/nodes/question-classifier/components/class-list.tsx": {
"react/set-state-in-effect": {
"count": 1
}
},
"web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts": {
"ts/no-explicit-any": {
"count": 8
@@ -4053,6 +4108,11 @@
"count": 1
}
},
"web/app/components/workflow/panel/__tests__/index.spec.tsx": {
"react/static-components": {
"count": 2
}
},
"web/app/components/workflow/panel/chat-record/index.tsx": {
"ts/no-explicit-any": {
"count": 8
@@ -4144,7 +4204,7 @@
},
"web/app/components/workflow/run/index.tsx": {
"react/set-state-in-effect": {
"count": 2
"count": 1
}
},
"web/app/components/workflow/run/iteration-log/index.tsx": {
@@ -4464,9 +4524,6 @@
}
},
"web/hooks/use-mitt.ts": {
"react/component-hook-factories": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}
@@ -4777,11 +4834,6 @@
"count": 1
}
},
"web/utils/context.ts": {
"react/component-hook-factories": {
"count": 1
}
},
"web/utils/error-parser.ts": {
"no-console": {
"count": 1

View File

@@ -2,7 +2,7 @@
"name": "dify",
"type": "module",
"private": true,
"packageManager": "pnpm@11.0.8",
"packageManager": "pnpm@11.1.1",
"engines": {
"node": "^22.22.1"
},

2509
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,10 @@ overrides:
'@monaco-editor/loader': 1.7.0
brace-expansion@>=2.0.0 <2.0.3: 2.0.3
canvas: ^3.2.2
dompurify@>=3.1.3 <=3.3.1: 3.3.2
dompurify@<3.4.0: ^3.4.0
dompurify@<=3.3.3: ^3.3.4
dompurify@>=1.0.10 <3.4.0: ^3.4.0
dompurify@>=3.0.1 <3.4.0: ^3.4.0
esbuild@<0.27.2: 0.27.2
flatted@<=3.4.1: 3.4.2
glob@>=10.2.0 <10.5.0: 11.1.0
@@ -39,6 +42,7 @@ overrides:
lodash@>=4.0.0 <= 4.17.23: 4.18.0
picomatch@<2.3.2: 2.3.2
picomatch@>=4.0.0 <4.0.4: 4.0.4
postcss@<8.5.10: ^8.5.10
rollup@>=4.0.0 <4.59.0: 4.59.0
safe-buffer: ^5.2.1
safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44
@@ -54,21 +58,21 @@ overrides:
yaml@>=2.0.0 <2.8.3: 2.8.3
yauzl@<3.2.1: 3.2.1
catalog:
'@amplitude/analytics-browser': 2.42.1
'@amplitude/plugin-session-replay-browser': 1.30.1
'@antfu/eslint-config': 8.2.0
'@amplitude/analytics-browser': 2.42.2
'@amplitude/plugin-session-replay-browser': 1.30.3
'@antfu/eslint-config': 9.0.0
'@base-ui/react': 1.4.1
'@chromatic-com/storybook': 5.1.2
'@cucumber/cucumber': 12.8.2
'@cucumber/cucumber': 12.8.3
'@egoist/tailwindcss-icons': 1.9.2
'@emoji-mart/data': 1.2.1
'@eslint-react/eslint-plugin': 3.0.0
'@eslint-react/eslint-plugin': 5.7.7
'@eslint/js': 10.0.1
'@floating-ui/react': 0.27.19
'@formatjs/intl-localematcher': 0.8.6
'@formatjs/intl-localematcher': 0.8.7
'@heroicons/react': 2.2.0
'@hey-api/openapi-ts': 0.97.1
'@hono/node-server': 2.0.1
'@hono/node-server': 2.0.2
'@iconify-json/heroicons': 1.2.3
'@iconify-json/ri': 1.2.10
'@lexical/code': 0.44.0
@@ -84,14 +88,14 @@ catalog:
'@monaco-editor/react': 4.7.0
'@next/eslint-plugin-next': 16.2.6
'@next/mdx': 16.2.6
'@orpc/client': 1.14.2
'@orpc/contract': 1.14.2
'@orpc/openapi-client': 1.14.2
'@orpc/tanstack-query': 1.14.2
'@playwright/test': 1.59.1
'@orpc/client': 1.14.3
'@orpc/contract': 1.14.3
'@orpc/openapi-client': 1.14.3
'@orpc/tanstack-query': 1.14.3
'@playwright/test': 1.60.0
'@remixicon/react': 4.9.0
'@rgrove/parse-xml': 4.2.0
'@sentry/react': 10.52.0
'@sentry/react': 10.53.1
'@storybook/addon-docs': 10.3.6
'@storybook/addon-links': 10.3.6
'@storybook/addon-onboarding': 10.3.6
@@ -102,42 +106,42 @@ catalog:
'@streamdown/math': 1.0.2
'@svgdotjs/svg.js': 3.2.5
'@t3-oss/env-nextjs': 0.13.11
'@tailwindcss/postcss': 4.2.4
'@tailwindcss/postcss': 4.3.0
'@tailwindcss/typography': 0.5.19
'@tailwindcss/vite': 4.2.4
'@tanstack/eslint-plugin-query': 5.100.9
'@tanstack/react-devtools': 0.10.2
'@tanstack/react-form': 1.29.1
'@tanstack/react-form-devtools': 0.2.22
'@tailwindcss/vite': 4.3.0
'@tanstack/eslint-plugin-query': 5.100.10
'@tanstack/react-devtools': 0.10.3
'@tanstack/react-form': 1.32.0
'@tanstack/react-form-devtools': 0.2.27
'@tanstack/react-hotkeys': 0.10.0
'@tanstack/react-query': 5.100.9
'@tanstack/react-query-devtools': 5.100.9
'@tanstack/react-query': 5.100.10
'@tanstack/react-query-devtools': 5.100.10
'@tanstack/react-virtual': 3.13.24
'@testing-library/dom': 10.4.1
'@testing-library/jest-dom': 6.9.1
'@testing-library/react': 16.3.2
'@testing-library/user-event': 14.6.1
'@tsslint/cli': 3.1.1
'@tsslint/compat-eslint': 3.1.1
'@tsslint/config': 3.1.1
'@tsslint/cli': 3.1.2
'@tsslint/compat-eslint': 3.1.2
'@tsslint/config': 3.1.2
'@types/js-cookie': 3.0.6
'@types/js-yaml': 4.0.9
'@types/negotiator': 0.6.4
'@types/node': 25.6.2
'@types/node': 25.7.0
'@types/qs': 6.15.1
'@types/react': 19.2.14
'@types/react-dom': 19.2.3
'@types/sortablejs': 1.15.9
'@typescript-eslint/eslint-plugin': 8.59.2
'@typescript-eslint/parser': 8.59.2
'@typescript/native-preview': 7.0.0-dev.20260507.1
'@typescript-eslint/eslint-plugin': 8.59.3
'@typescript-eslint/parser': 8.59.3
'@typescript/native-preview': 7.0.0-dev.20260512.1
'@vitejs/plugin-react': 6.0.1
'@vitejs/plugin-rsc': 0.5.26
'@vitest/coverage-v8': 4.1.5
'@vitest/coverage-v8': 4.1.6
abcjs: 6.6.3
agentation: 3.0.2
ahooks: 3.9.7
c12: 1.11.2
c12: 4.0.0-beta.5
class-variance-authority: 0.7.1
client-only: 0.0.1
clsx: 2.1.1
@@ -172,22 +176,22 @@ catalog:
hono: 4.12.18
html-entities: 2.6.0
html-to-image: 1.11.13
i18next: 26.0.10
i18next: 26.1.0
i18next-resources-to-backend: 1.2.1
iconify-import-svg: 0.2.0
immer: 11.1.7
immer: 11.1.8
jotai: 2.20.0
js-audio-recorder: 1.0.7
js-cookie: 3.0.5
js-yaml: 4.1.1
jsonschema: 1.5.0
katex: 0.16.45
knip: 6.12.1
knip: 6.13.1
ky: 2.0.2
lamejs: 1.2.1
lexical: 0.44.0
loro-crdt: 1.12.1
mermaid: 11.14.0
mermaid: 11.15.0
mime: 4.1.0
mitt: 3.0.1
negotiator: 1.0.0
@@ -195,7 +199,7 @@ catalog:
next-themes: 0.4.6
nuqs: 2.8.9
pinyin-pro: 3.28.1
playwright: 1.59.1
playwright: 1.60.0
postcss: 8.5.14
qrcode.react: 4.2.0
qs: 6.15.1
@@ -223,8 +227,8 @@ catalog:
storybook: 10.3.6
streamdown: 2.5.0
string-ts: 2.3.1
tailwind-merge: 3.5.0
tailwindcss: 4.2.4
tailwind-merge: 3.6.0
tailwindcss: 4.3.0
tldts: 7.0.30
tsx: 4.21.0
typescript: 6.0.3

View File

@@ -14,7 +14,7 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { DSLExportConfirmContent } from '@/app/components/workflow/dsl-export-confirm-modal'
import dynamic from '@/next/dynamic'
@@ -157,7 +157,14 @@ const AppInfoModals = ({
</AlertDialogDescription>
<div className="mt-2">
<label className="mb-1 block system-sm-regular text-text-secondary">
{t('deleteAppConfirmInputLabel', { ns: 'app', appName: appDetail.name })}
<Trans
i18nKey="deleteAppConfirmInputLabel"
ns="app"
values={{ appName: appDetail.name }}
components={{
appName: <span className="system-sm-semibold text-text-primary" translate="no" />,
}}
/>
</label>
<Input
type="text"

View File

@@ -44,7 +44,7 @@ type PlaygroundFormFieldsProps = {
const PlaygroundFormFields = ({ form, status }: PlaygroundFormFieldsProps) => {
type PlaygroundFormValues = typeof demoFormOpts.defaultValues
const name = useStore(form.store, state => (state.values as PlaygroundFormValues).name)
const contactFormApi = form as ContactFieldsFormApi
const contactFormApi = form as unknown as ContactFieldsFormApi
return (
<form

View File

@@ -9,6 +9,7 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
} from 'react'
import { HITL_INPUT_REG } from '@/config'
import { decoratorTransform } from '../../utils'
@@ -39,6 +40,32 @@ const HITLInputReplacementBlock = ({
acc.push(...curr.vars.filter(v => v.isRagVariable))
return acc
}, []), [variables])
const latestConfigRef = useRef({
nodeId,
formInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
workflowNodesMap,
getVarType,
environmentVariables,
conversationVariables,
ragVariables,
readonly,
})
latestConfigRef.current = {
nodeId,
formInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
workflowNodesMap,
getVarType,
environmentVariables,
conversationVariables,
ragVariables,
readonly,
}
useEffect(() => {
if (!editor.hasNodes([HITLInputNode]))
@@ -47,6 +74,20 @@ const HITLInputReplacementBlock = ({
const createHITLInputBlockNode = useCallback((textNode: TextNode): HITLInputNode => {
const varName = textNode.getTextContent().split('.')[1]!.replace(/#\}\}$/, '')
const {
nodeId,
formInputs,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
workflowNodesMap,
getVarType,
environmentVariables,
conversationVariables,
ragVariables,
readonly,
} = latestConfigRef.current
return $applyNodeReplacement($createHITLInputNode(
varName,
nodeId,
@@ -61,7 +102,7 @@ const HITLInputReplacementBlock = ({
ragVariables,
readonly,
))
}, [nodeId, formInputs, onFormInputsChange, onFormInputItemRename, onFormInputItemRemove, workflowNodesMap, getVarType, environmentVariables, conversationVariables, ragVariables, readonly])
}, [])
const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)

View File

@@ -188,7 +188,7 @@ describe('ShortcutsPopupPlugin', () => {
const portalContent = await screen.findByText(SHORTCUTS_EMPTY_CONTENT)
const floatingDiv = portalContent.closest('div')
expect(document.body).toContainElement(portalContent)
expect(floatingDiv).toHaveClass('z-1002')
expect(floatingDiv).toHaveStyle({ zIndex: '50' })
})
// ─── matchHotkey: string hotkey ───

View File

@@ -160,8 +160,7 @@ export default function ShortcutsPopupPlugin({
Object.assign(elements.floating.style, {
maxWidth: `${Math.min(400, availableWidth)}px`,
maxHeight: `${Math.max(0, availableHeight)}px`,
overflowX: 'hidden',
overflowY: 'auto',
overflow: 'visible',
})
},
padding: 8,
@@ -300,12 +299,13 @@ export default function ShortcutsPopupPlugin({
refs.setFloating(node)
}}
className={cn(
useContainer ? '' : 'z-1002',
'absolute rounded-xl bg-components-panel-bg-blur shadow-lg',
className,
)}
style={{
...floatingStyles,
zIndex: useContainer ? undefined : 50,
overflow: 'visible',
visibility: isPositioned ? 'visible' : 'hidden',
}}
>

View File

@@ -1,9 +1,6 @@
import type {
EditorState,
LexicalCommand,
} from 'lexical'
import type { EditorState } from 'lexical'
import type { FC } from 'react'
import type { Hotkey } from './plugins/shortcuts-popup-plugin'
import type { Hotkey, ShortcutPopupInsertHandler } from './plugins/shortcuts-popup-plugin'
import type {
ContextBlockType,
CurrentBlockType,
@@ -71,7 +68,7 @@ import {
type ShortcutPopup = {
hotkey: Hotkey
Popup: React.ComponentType<{ onClose: () => void, onInsert: (command: LexicalCommand<unknown>, params: unknown[]) => void }>
Popup: React.ComponentType<{ onClose: () => void, onInsert: ShortcutPopupInsertHandler }>
}
type PromptEditorContentProps = {

View File

@@ -118,6 +118,23 @@ describe('ModelSelector', () => {
expect(triggerButton).toHaveAttribute('aria-expanded', 'false')
})
it('should use the default model settings popup width when the trigger is narrow', () => {
renderWithQueryClient(
<div className="w-[355px]">
<ModelSelector modelList={[makeModel()]} />
</div>,
)
fireEvent.click(screen.getByRole('combobox'))
expect(
Array.from(document.body.querySelectorAll('[class]')).some(element =>
element.className.includes('w-[432px]')
&& element.className.includes('max-w-[432px]'),
),
).toBe(true)
})
it('should not open popup when readonly', () => {
renderWithQueryClient(<ModelSelector modelList={[makeModel()]} readonly />)

View File

@@ -1,6 +1,7 @@
import type { ReactElement, ReactNode } from 'react'
import type { DefaultModel, Model, ModelItem } from '../../declarations'
import { Combobox } from '@langgenius/dify-ui/combobox'
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import { fireEvent, render, screen } from '@testing-library/react'
import {
ConfigurationMethodEnum,
@@ -106,6 +107,11 @@ const makeProvider = (overrides: Record<string, unknown> = {}) => ({
...overrides,
})
const previewCardProps = () => ({
previewCardHandle: createPreviewCardHandle(),
onPreviewCardClose: vi.fn(),
})
const createComboboxNode = (
node: ReactElement,
onValueChange = vi.fn(),
@@ -152,7 +158,7 @@ describe('PopupItem', () => {
})
const { container } = renderWithCombobox(
<PopupItem model={makeModel()} onHide={vi.fn()} />,
<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />,
)
expect(container.textContent).toBe('')
@@ -160,7 +166,7 @@ describe('PopupItem', () => {
it('should select the combobox value when clicking an active model', () => {
const onValueChange = vi.fn()
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />, onValueChange)
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />, onValueChange)
fireEvent.click(screen.getByText('GPT-4'))
@@ -170,10 +176,27 @@ describe('PopupItem', () => {
)
})
it('should close the shared preview before pressing an active model', () => {
const onPreviewCardClose = vi.fn()
renderWithCombobox(
<PopupItem
previewCardHandle={createPreviewCardHandle()}
onPreviewCardClose={onPreviewCardClose}
model={makeModel()}
onHide={vi.fn()}
/>,
)
fireEvent.pointerDown(screen.getByText('GPT-4'))
expect(onPreviewCardClose).toHaveBeenCalledTimes(1)
})
it('should not select the combobox value when model is not active', () => {
const onValueChange = vi.fn()
renderWithCombobox(
<PopupItem
{...previewCardProps()}
model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.disabled })] })}
onHide={vi.fn()}
/>,
@@ -188,7 +211,7 @@ describe('PopupItem', () => {
it('should open model modal when clicking add on unconfigured model', () => {
const onValueChange = vi.fn()
const { rerender } = renderWithCombobox(
<PopupItem model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })} onHide={vi.fn()} />,
<PopupItem {...previewCardProps()} model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })} onHide={vi.fn()} />,
onValueChange,
)
@@ -206,6 +229,7 @@ describe('PopupItem', () => {
rerender(createComboboxNode(
<PopupItem
{...previewCardProps()}
model={makeModel({
models: [makeModelItem({ status: ModelStatusEnum.noConfigure, model_type: undefined as unknown as ModelTypeEnum })],
})}
@@ -225,6 +249,7 @@ describe('PopupItem', () => {
const defaultModel: DefaultModel = { provider: 'openai', model: 'gpt-4' }
renderWithCombobox(
<PopupItem
{...previewCardProps()}
defaultModel={defaultModel}
model={makeModel()}
onHide={vi.fn()}
@@ -239,6 +264,7 @@ describe('PopupItem', () => {
renderWithCombobox(
<PopupItem
{...previewCardProps()}
model={makeModel({
label: { en_US: 'OpenAI only' } as Model['label'],
models: [makeModelItem({ label: { en_US: 'GPT-4 only' } as ModelItem['label'] })],
@@ -252,7 +278,7 @@ describe('PopupItem', () => {
})
it('should toggle collapsed state when clicking provider header', () => {
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
expect(screen.getByText('GPT-4'))!.toBeInTheDocument()
@@ -266,7 +292,7 @@ describe('PopupItem', () => {
})
it('should show credential name when using custom provider', () => {
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
expect(screen.getByText('my-api-key'))!.toBeInTheDocument()
})
@@ -283,7 +309,7 @@ describe('PopupItem', () => {
credits: 200,
})
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
expect(screen.getByText('stale-key'))!.toBeInTheDocument()
expect(document.querySelector('.bg-components-badge-status-light-error-bg')).not.toBeNull()
@@ -309,7 +335,7 @@ describe('PopupItem', () => {
credits: 0,
})
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
expect(screen.getByText(/modelProvider\.selector\.configureRequired/))!.toBeInTheDocument()
})
@@ -331,7 +357,7 @@ describe('PopupItem', () => {
credits: 200,
})
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
expect(screen.getByText(/modelProvider\.selector\.aiCredits/))!.toBeInTheDocument()
})
@@ -356,7 +382,7 @@ describe('PopupItem', () => {
credits: 0,
})
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
expect(screen.getByText(/modelProvider\.selector\.creditsExhausted/))!.toBeInTheDocument()
})
@@ -364,7 +390,7 @@ describe('PopupItem', () => {
it('should close the dropdown through dropdown content callbacks', () => {
const onHide = vi.fn()
renderWithCombobox(<PopupItem model={makeModel()} onHide={onHide} />)
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={onHide} />)
fireEvent.click(screen.getByRole('button', { name: /my-api-key/ }))
fireEvent.click(screen.getByRole('button', { name: 'close dropdown' }))

View File

@@ -130,7 +130,7 @@ function ModelSelector({
<ComboboxContent
placement="bottom-start"
sideOffset={4}
popupClassName={cn('min-w-[320px] overflow-hidden rounded-xl', popupClassName)}
popupClassName={cn('w-[432px] max-w-[432px] overflow-hidden rounded-xl', popupClassName)}
>
<Popup
defaultModel={defaultModel}

View File

@@ -1,32 +1,41 @@
import type { DefaultModel, Model } from '../declarations'
import type { ComponentProps } from 'react'
import type { DefaultModel, Model, ModelItem } from '../declarations'
import { cn } from '@langgenius/dify-ui/cn'
import { ComboboxGroup, ComboboxItem, ComboboxItemIndicator } from '@langgenius/dify-ui/combobox'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { ConfigurationMethodEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations'
import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations'
import { useLanguage, useUpdateModelList, useUpdateModelProviders } from '../hooks'
import ModelBadge from '../model-badge'
import ModelIcon from '../model-icon'
import ModelName from '../model-name'
import DropdownContent from '../provider-added-card/model-auth-dropdown/dropdown-content'
import { useChangeProviderPriority } from '../provider-added-card/use-change-provider-priority'
import { useCredentialPanelState } from '../provider-added-card/use-credential-panel-state'
import { modelTypeFormat, sizeFormat } from '../utils'
import FeatureIcon from './feature-icon'
export type ModelSelectorPreviewPayload = {
provider: Model
modelItem: ModelItem
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
type PopupItemProps = {
defaultModel?: DefaultModel
model: Model
previewCardHandle: PreviewCardHandle
onPreviewCardClose: () => void
onHide: () => void
}
function PopupItem({
defaultModel,
model,
previewCardHandle,
onPreviewCardClose,
onHide,
}: PopupItemProps) {
const [collapsed, setCollapsed] = useState(false)
@@ -167,7 +176,11 @@ function PopupItem({
)
const itemRender = modelItem.status === ModelStatusEnum.noConfigure
? (
<div className={rowClassName} aria-disabled="true">
<div
className={rowClassName}
aria-disabled="true"
onPointerDown={onPreviewCardClose}
>
{rowContent}
<button
type="button"
@@ -186,67 +199,21 @@ function PopupItem({
}}
disabled={modelItem.status !== ModelStatusEnum.active}
className={rowClassName}
onPointerDown={onPreviewCardClose}
>
{rowContent}
</ComboboxItem>
)
return (
<PreviewCard key={modelItem.model}>
<PreviewCardTrigger
delay={150}
closeDelay={150}
render={itemRender}
/>
<PreviewCardContent
placement="right"
popupClassName="w-[206px] bg-components-panel-bg-blur p-3 shadow-none backdrop-blur-xs"
>
<div className="flex flex-col gap-1">
<div className="flex flex-col items-start gap-2">
<ModelIcon
className={cn('h-5 w-5 shrink-0')}
provider={model}
modelName={modelItem.model}
/>
<div className="system-md-medium text-wrap wrap-break-word text-text-primary">{modelItem.label[language] || modelItem.label.en_US}</div>
</div>
<div className="flex flex-wrap gap-1">
{!!modelItem.model_type && (
<ModelBadge>
{modelTypeFormat(modelItem.model_type)}
</ModelBadge>
)}
{!!modelItem.model_properties.mode && (
<ModelBadge>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge>
)}
{!!modelItem.model_properties.context_size && (
<ModelBadge>
{sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge>
)}
</div>
{[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum)
&& modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature))
&& (
<div className="pt-2">
<div className="mb-1 system-2xs-medium-uppercase text-text-tertiary">{t('model.capabilities', { ns: 'common' })}</div>
<div className="flex flex-wrap gap-1">
{modelItem.features?.map(feature => (
<FeatureIcon
key={feature}
feature={feature}
showFeaturesLabel
/>
))}
</div>
</div>
)}
</div>
</PreviewCardContent>
</PreviewCard>
<PreviewCardTrigger
key={modelItem.model}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ provider: model, modelItem }}
render={itemRender}
/>
)
})}
</ComboboxGroup>

View File

@@ -1,6 +1,8 @@
import type { DefaultModel, Model, ModelFeatureEnum } from '../declarations'
import type { DefaultModel, Model } from '../declarations'
import type { ModelSelectorPreviewPayload } from './popup-item'
import type { ModelProviderQuotaGetPaid } from '@/types/model-provider'
import { ComboboxList } from '@langgenius/dify-ui/combobox'
import { createPreviewCardHandle, PreviewCard, PreviewCardContent } from '@langgenius/dify-ui/preview-card'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useTheme } from 'next-themes'
import { useCallback, useMemo, useState } from 'react'
@@ -12,12 +14,15 @@ import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
import { CustomConfigurationStatusEnum, ModelStatusEnum } from '../declarations'
import { CustomConfigurationStatusEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations'
import { useLanguage, useMarketplaceAllPlugins } from '../hooks'
import ModelBadge from '../model-badge'
import ModelIcon from '../model-icon'
import CreditsExhaustedAlert from '../provider-added-card/model-auth-dropdown/credits-exhausted-alert'
import { useTrialCredits } from '../provider-added-card/use-trial-credits'
import { providerSupportsCredits } from '../supports-credits'
import { MODEL_PROVIDER_QUOTA_GET_PAID, providerKeyToPluginId } from '../utils'
import { MODEL_PROVIDER_QUOTA_GET_PAID, modelTypeFormat, providerKeyToPluginId, sizeFormat } from '../utils'
import FeatureIcon from './feature-icon'
import MarketplaceSection from './marketplace-section'
import { createModelSelectorSearchIndex, filterModelSelectorModels } from './model-search'
import ModelSelectorEmptyState from './popup-empty-state'
@@ -43,6 +48,7 @@ function Popup({
const { t } = useTranslation()
const { theme } = useTheme()
const language = useLanguage()
const previewCardHandle = useMemo(() => createPreviewCardHandle<ModelSelectorPreviewPayload>(), [])
const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
const { setShowAccountSettingModal } = useModalContext()
const { modelProviders } = useProviderContext()
@@ -151,6 +157,9 @@ function Popup({
onHide()
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
}, [onHide, setShowAccountSettingModal])
const handleClosePreviewCard = useCallback(() => {
previewCardHandle.close()
}, [previewCardHandle])
return (
<ModelSelectorPopupFrame>
@@ -170,6 +179,8 @@ function Popup({
key={model.provider}
defaultModel={defaultModel}
model={model}
previewCardHandle={previewCardHandle}
onPreviewCardClose={handleClosePreviewCard}
onHide={onHide}
/>
))
@@ -201,9 +212,86 @@ function Popup({
/>
</div>
</ModelSelectorScrollBody>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<ModelSelectorPreviewCard
capabilitiesLabel={t('model.capabilities', { ns: 'common' })}
language={language}
payload={payload as ModelSelectorPreviewPayload | undefined}
/>
)}
</PreviewCard>
<ModelProviderSettingsFooter onOpenSettings={handleOpenSettings} />
</ModelSelectorPopupFrame>
)
}
type ModelSelectorPreviewCardProps = {
capabilitiesLabel: string
language: string
payload?: ModelSelectorPreviewPayload
}
function ModelSelectorPreviewCard({
capabilitiesLabel,
language,
payload,
}: ModelSelectorPreviewCardProps) {
if (!payload)
return null
const { provider, modelItem } = payload
return (
<PreviewCardContent
placement="right"
popupClassName="w-[206px] bg-components-panel-bg-blur p-3 shadow-none backdrop-blur-xs"
>
<div className="flex flex-col gap-1">
<div className="flex flex-col items-start gap-2">
<ModelIcon
className="h-5 w-5 shrink-0"
provider={provider}
modelName={modelItem.model}
/>
<div className="system-md-medium text-wrap wrap-break-word text-text-primary">{modelItem.label[language] || modelItem.label.en_US}</div>
</div>
<div className="flex flex-wrap gap-1">
{!!modelItem.model_type && (
<ModelBadge>
{modelTypeFormat(modelItem.model_type)}
</ModelBadge>
)}
{!!modelItem.model_properties.mode && (
<ModelBadge>
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
</ModelBadge>
)}
{!!modelItem.model_properties.context_size && (
<ModelBadge>
{sizeFormat(modelItem.model_properties.context_size as number)}
</ModelBadge>
)}
</div>
{[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum)
&& modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature))
&& (
<div className="pt-2">
<div className="mb-1 system-2xs-medium-uppercase text-text-tertiary">{capabilitiesLabel}</div>
<div className="flex flex-wrap gap-1">
{modelItem.features?.map(feature => (
<FeatureIcon
key={feature}
feature={feature}
showFeaturesLabel
/>
))}
</div>
</div>
)}
</div>
</PreviewCardContent>
)
}
export default Popup

View File

@@ -1,11 +1,11 @@
import type { FC } from 'react'
import { useTranslation } from '#i18n'
import { cn } from '@langgenius/dify-ui/cn'
import { RiAlertFill } from '@remixicon/react'
import { camelCase } from 'es-toolkit/string'
import * as React from 'react'
import { useMemo } from 'react'
import { Trans } from 'react-i18next'
import { useTranslation } from '#i18n'
import Link from '@/next/link'
type DeprecationNoticeProps = {

View File

@@ -1,9 +1,9 @@
'use client'
import type { Plugin } from '../types'
import { useTranslation } from '#i18n'
import { cn } from '@langgenius/dify-ui/cn'
import { RiAlertFill } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from '#i18n'
import { useSelector } from '@/context/app-context'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'

View File

@@ -1,6 +1,6 @@
'use client'
import { useTranslation } from '#i18n'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from '#i18n'
import { Group } from '@/app/components/base/icons/src/vender/other'
import Line from './line'

View File

@@ -1,12 +1,12 @@
'use client'
import type { Plugin } from '@/app/components/plugins/types'
import { useLocale, useTranslation } from '#i18n'
import { Button } from '@langgenius/dify-ui/button'
import { RiArrowRightUpLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { useTheme } from 'next-themes'
import * as React from 'react'
import { useMemo } from 'react'
import { useLocale, useTranslation } from '#i18n'
import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import { useTags } from '@/app/components/plugins/hooks'

View File

@@ -2,9 +2,9 @@
import type { MarketplaceCollection } from '../types'
import type { Plugin } from '@/app/components/plugins/types'
import { useLocale, useTranslation } from '#i18n'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowRightSLine } from '@remixicon/react'
import { useLocale, useTranslation } from '#i18n'
import { getLanguage } from '@/i18n-config/language'
import { useMarketplaceMoreClick } from '../atoms'
import CardWrapper from './card-wrapper'

View File

@@ -1,6 +1,5 @@
'use client'
import type { ActivePluginType } from './constants'
import { useTranslation } from '#i18n'
import { cn } from '@langgenius/dify-ui/cn'
import {
RiArchive2Line,
@@ -11,6 +10,7 @@ import {
RiSpeakAiLine,
} from '@remixicon/react'
import { useSetAtom } from 'jotai'
import { useTranslation } from '#i18n'
import { Trigger as TriggerIcon } from '@/app/components/base/icons/src/vender/plugin'
import { searchModeAtom, useActivePluginType } from './atoms'
import { PLUGIN_CATEGORY_WITH_COLLECTIONS, PLUGIN_TYPE_SEARCH_MAP } from './constants'

View File

@@ -1,12 +1,12 @@
'use client'
import { useTranslation } from '#i18n'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { useState } from 'react'
import { useTranslation } from '#i18n'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
import { useTags } from '@/app/components/plugins/hooks'

View File

@@ -1,8 +1,8 @@
import type { Tag } from '../../../hooks'
import { useTranslation } from '#i18n'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowDownSLine, RiCloseCircleFill, RiFilter3Line } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from '#i18n'
type MarketplaceTriggerProps = {
selectedTagsLength: number

View File

@@ -1,5 +1,4 @@
'use client'
import { useTranslation } from '#i18n'
import {
DropdownMenu,
DropdownMenuContent,
@@ -7,6 +6,7 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useState } from 'react'
import { useTranslation } from '#i18n'
import { useMarketplaceSort } from '../atoms'
const SortDropdown = () => {

View File

@@ -1,11 +1,11 @@
import type { useMarketplace } from './hooks'
import { useLocale } from '#i18n'
import {
RiArrowRightUpLine,
RiArrowUpDoubleLine,
} from '@remixicon/react'
import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
import { useLocale } from '#i18n'
import Loading from '@/app/components/base/loading'
import List from '@/app/components/plugins/marketplace/list'
import { getMarketplaceUrl } from '@/utils/var'

View File

@@ -1,6 +1,7 @@
import type { NodeDefault } from '../types'
import type { BlockClassificationEnum } from './types'
import {
createPreviewCardHandle,
PreviewCard,
PreviewCardContent,
PreviewCardTrigger,
@@ -27,6 +28,10 @@ type BlocksProps = {
availableBlocksTypes?: BlockEnum[]
blocks?: NodeDefault[]
}
type BlockPreviewPayload = {
block: NodeDefault
}
const Blocks = ({
searchText,
onSelect,
@@ -43,6 +48,7 @@ const Blocks = ({
return filterEvaluationWorkflowRestrictedBlockTypes(availableBlocksTypes)
}, [appType, availableBlocksTypes])
const previewCardHandle = useMemo(() => createPreviewCardHandle<BlockPreviewPayload>(), [])
// Use external blocks if provided, otherwise fallback to hook-based blocks
const blocks = blocksFromProps || blocksFromHooks.map(block => ({
@@ -110,51 +116,38 @@ const Blocks = ({
// hover/focus-only activation is a11y-safe. See
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
filteredList.map(block => (
<PreviewCard key={block.metaData.type}>
<PreviewCardTrigger
delay={150}
closeDelay={150}
render={(
<div
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
onClick={() => onSelect(block.metaData.type)}
>
<BlockIcon
className="mr-2 shrink-0"
type={block.metaData.type}
/>
<div className="grow text-sm text-text-secondary">{block.metaData.title}</div>
{
block.metaData.type === BlockEnum.LoopEnd && (
<Badge
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
className="ml-2 shrink-0"
/>
)
}
</div>
)}
/>
<PreviewCardContent
placement="right"
popupClassName="w-[200px] border-none px-3 py-2"
>
<div>
<PreviewCardTrigger
key={block.metaData.type}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ block }}
render={(
<div
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
onClick={() => onSelect(block.metaData.type)}
>
<BlockIcon
size="md"
className="mb-2"
className="mr-2 shrink-0"
type={block.metaData.type}
/>
<div className="mb-1 system-md-medium text-text-primary">{block.metaData.title}</div>
<div className="system-xs-regular text-text-tertiary">{block.metaData.description}</div>
<div className="grow text-sm text-text-secondary">{block.metaData.title}</div>
{
block.metaData.type === BlockEnum.LoopEnd && (
<Badge
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
className="ml-2 shrink-0"
/>
)
}
</div>
</PreviewCardContent>
</PreviewCard>
)}
/>
))
}
</div>
)
}, [groups, onSelect, t, store])
}, [groups, onSelect, previewCardHandle, t, store])
return (
<div className="max-h-[480px] max-w-[500px] overflow-y-auto p-1">
@@ -166,8 +159,43 @@ const Blocks = ({
{
!isEmpty && BLOCK_CLASSIFICATIONS.map(renderGroup)
}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<BlockPreviewCard payload={payload as BlockPreviewPayload | undefined} />
)}
</PreviewCard>
</div>
)
}
type BlockPreviewCardProps = {
payload?: BlockPreviewPayload
}
function BlockPreviewCard({
payload,
}: BlockPreviewCardProps) {
if (!payload)
return null
const { block } = payload
return (
<PreviewCardContent
placement="right"
popupClassName="w-[200px] border-none px-3 py-2"
>
<div>
<BlockIcon
size="md"
className="mb-2"
type={block.metaData.type}
/>
<div className="mb-1 system-md-medium text-text-primary">{block.metaData.title}</div>
<div className="system-xs-regular wrap-break-word text-text-tertiary">{block.metaData.description}</div>
</div>
</PreviewCardContent>
)
}
export default memo(Blocks)

View File

@@ -1,9 +1,10 @@
'use client'
import type { TFunction } from 'i18next'
import type { ToolWithProvider } from '../types'
import type { ToolDefaultValue, ToolValue } from './types'
import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { RiMoreLine } from '@remixicon/react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -33,6 +34,11 @@ type FeaturedToolsProps = {
isLoading?: boolean
onInstallSuccess?: () => void
}
type FeaturedToolPreviewPayload = {
plugin: Plugin
label: string
description: string
}
const STORAGE_KEY = 'workflow_tools_featured_collapsed'
@@ -46,7 +52,9 @@ const FeaturedTools = ({
}: FeaturedToolsProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const previewCardHandle = useMemo(() => createPreviewCardHandle<FeaturedToolPreviewPayload>(), [])
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins)
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
if (isServer)
return false
@@ -54,23 +62,16 @@ const FeaturedTools = ({
return stored === 'true'
})
useEffect(() => {
if (isServer)
return
const stored = window.localStorage.getItem(STORAGE_KEY)
if (stored !== null)
setIsCollapsed(stored === 'true')
}, [])
useEffect(() => {
if (isServer)
return
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
}, [isCollapsed])
useEffect(() => {
if (visibleCountPlugins !== plugins) {
setVisibleCountPlugins(plugins)
setVisibleCount(INITIAL_VISIBLE_COUNT)
}, [plugins])
}
const limitedPlugins = useMemo(
() => plugins.slice(0, MAX_RECOMMENDED_COUNT),
@@ -174,10 +175,11 @@ const FeaturedTools = ({
key={plugin.plugin_id}
plugin={plugin}
language={language}
previewCardHandle={previewCardHandle}
onInstallSuccess={async () => {
await onInstallSuccess?.()
}}
t={t as any}
t={t}
/>
))}
</div>
@@ -214,6 +216,11 @@ const FeaturedTools = ({
)}
</>
)}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<FeaturedToolPreviewCard payload={payload as FeaturedToolPreviewPayload | undefined} />
)}
</PreviewCard>
</div>
)
}
@@ -221,13 +228,15 @@ const FeaturedTools = ({
type FeaturedToolUninstalledItemProps = {
plugin: Plugin
language: Locale
previewCardHandle: ReturnType<typeof createPreviewCardHandle<FeaturedToolPreviewPayload>>
onInstallSuccess?: () => Promise<void> | void
t: (key: string, options?: Record<string, any>) => string
t: TFunction
}
function FeaturedToolUninstalledItem({
plugin,
language,
previewCardHandle,
onInstallSuccess,
t,
}: FeaturedToolUninstalledItemProps) {
@@ -296,16 +305,13 @@ function FeaturedToolUninstalledItem({
// Preview is supplementary: icon / label / brief are all reachable from
// the InstallFromMarketplace modal that opens on click, so hover/focus-only
// activation is a11y-safe. See packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
<PreviewCard>
<PreviewCardTrigger delay={150} closeDelay={150} render={row} />
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
<div>
<BlockIcon size="md" className="mb-2" type={BlockEnum.Tool} toolIcon={plugin.icon} />
<div className="mb-1 text-sm leading-5 text-text-primary">{label}</div>
<div className="text-xs leading-[18px] text-text-secondary">{description}</div>
</div>
</PreviewCardContent>
</PreviewCard>
<PreviewCardTrigger
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ plugin, label, description }}
render={row}
/>
)
: row}
{isInstallModalOpen && (
@@ -325,4 +331,25 @@ function FeaturedToolUninstalledItem({
)
}
type FeaturedToolPreviewCardProps = {
payload?: FeaturedToolPreviewPayload
}
function FeaturedToolPreviewCard({
payload,
}: FeaturedToolPreviewCardProps) {
if (!payload)
return null
return (
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
<div>
<BlockIcon size="md" className="mb-2" type={BlockEnum.Tool} toolIcon={payload.plugin.icon} />
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label}</div>
<div className="text-xs leading-[18px] wrap-break-word text-text-secondary">{payload.description}</div>
</div>
</PreviewCardContent>
)
}
export default FeaturedTools

View File

@@ -1,8 +1,10 @@
'use client'
import type { TFunction } from 'i18next'
import type { TriggerPluginActionPreviewPayload } from './trigger-plugin/action-item'
import type { TriggerDefaultValue, TriggerWithProvider } from './types'
import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { RiMoreLine } from '@remixicon/react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -17,6 +19,7 @@ import { formatNumber } from '@/utils/format'
import { getMarketplaceUrl } from '@/utils/var'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import { TriggerPluginActionPreviewCard } from './trigger-plugin/action-item'
import TriggerPluginItem from './trigger-plugin/item'
const MAX_RECOMMENDED_COUNT = 15
@@ -29,6 +32,11 @@ type FeaturedTriggersProps = {
isLoading?: boolean
onInstallSuccess?: () => void | Promise<void>
}
type FeaturedTriggerPreviewPayload = {
plugin: Plugin
label: string
description: string
}
const STORAGE_KEY = 'workflow_triggers_featured_collapsed'
@@ -41,7 +49,10 @@ const FeaturedTriggers = ({
}: FeaturedTriggersProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const previewCardHandle = useMemo(() => createPreviewCardHandle<FeaturedTriggerPreviewPayload>(), [])
const triggerActionPreviewCardHandle = useMemo(() => createPreviewCardHandle<TriggerPluginActionPreviewPayload>(), [])
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
const [visibleCountPlugins, setVisibleCountPlugins] = useState(plugins)
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
if (isServer)
return false
@@ -49,23 +60,16 @@ const FeaturedTriggers = ({
return stored === 'true'
})
useEffect(() => {
if (isServer)
return
const stored = window.localStorage.getItem(STORAGE_KEY)
if (stored !== null)
setIsCollapsed(stored === 'true')
}, [])
useEffect(() => {
if (isServer)
return
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
}, [isCollapsed])
useEffect(() => {
if (visibleCountPlugins !== plugins) {
setVisibleCountPlugins(plugins)
setVisibleCount(INITIAL_VISIBLE_COUNT)
}, [plugins])
}
const limitedPlugins = useMemo(
() => plugins.slice(0, MAX_RECOMMENDED_COUNT),
@@ -156,6 +160,7 @@ const FeaturedTriggers = ({
key={provider.id}
payload={provider}
hasSearchText={false}
previewCardHandle={triggerActionPreviewCardHandle}
onSelect={onSelect}
/>
))}
@@ -169,10 +174,11 @@ const FeaturedTriggers = ({
key={plugin.plugin_id}
plugin={plugin}
language={language}
previewCardHandle={previewCardHandle}
onInstallSuccess={async () => {
await onInstallSuccess?.()
}}
t={t as any}
t={t}
/>
))}
</div>
@@ -209,6 +215,16 @@ const FeaturedTriggers = ({
)}
</>
)}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<FeaturedTriggerPreviewCard payload={payload as FeaturedTriggerPreviewPayload | undefined} />
)}
</PreviewCard>
<PreviewCard handle={triggerActionPreviewCardHandle}>
{({ payload }) => (
<TriggerPluginActionPreviewCard payload={payload as TriggerPluginActionPreviewPayload | undefined} />
)}
</PreviewCard>
</div>
)
}
@@ -216,13 +232,15 @@ const FeaturedTriggers = ({
type FeaturedTriggerUninstalledItemProps = {
plugin: Plugin
language: Locale
previewCardHandle: ReturnType<typeof createPreviewCardHandle<FeaturedTriggerPreviewPayload>>
onInstallSuccess?: () => Promise<void> | void
t: (key: string, options?: Record<string, any>) => string
t: TFunction
}
function FeaturedTriggerUninstalledItem({
plugin,
language,
previewCardHandle,
onInstallSuccess,
t,
}: FeaturedTriggerUninstalledItemProps) {
@@ -291,16 +309,13 @@ function FeaturedTriggerUninstalledItem({
// Preview is supplementary: icon / label / brief are all reachable from
// the InstallFromMarketplace modal that opens on click, so hover/focus-only
// activation is a11y-safe. See packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
<PreviewCard>
<PreviewCardTrigger delay={150} closeDelay={150} render={row} />
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
<div>
<BlockIcon size="md" className="mb-2" type={BlockEnum.TriggerPlugin} toolIcon={plugin.icon} />
<div className="mb-1 text-sm leading-5 text-text-primary">{label}</div>
<div className="text-xs leading-[18px] text-text-secondary">{description}</div>
</div>
</PreviewCardContent>
</PreviewCard>
<PreviewCardTrigger
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ plugin, label, description }}
render={row}
/>
)
: row}
{isInstallModalOpen && (
@@ -320,4 +335,25 @@ function FeaturedTriggerUninstalledItem({
)
}
type FeaturedTriggerPreviewCardProps = {
payload?: FeaturedTriggerPreviewPayload
}
function FeaturedTriggerPreviewCard({
payload,
}: FeaturedTriggerPreviewCardProps) {
if (!payload)
return null
return (
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
<div>
<BlockIcon size="md" className="mb-2" type={BlockEnum.TriggerPlugin} toolIcon={payload.plugin.icon} />
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label}</div>
<div className="text-xs leading-[18px] wrap-break-word text-text-secondary">{payload.description}</div>
</div>
</PreviewCardContent>
)
}
export default FeaturedTriggers

View File

@@ -1,11 +1,14 @@
import type { BlockEnum, ToolWithProvider } from '../../types'
import type { ToolActionPreviewPayload } from '../tool/action-item'
import type { ToolDefaultValue } from '../types'
import type { Plugin } from '@/app/components/plugins/types'
import type { OnSelectBlock } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card'
import { useCallback, useMemo, useRef } from 'react'
import { useGetLanguage } from '@/context/i18n'
import { groupItems } from '../index-bar'
import { ToolActionPreviewCard } from '../tool/action-item'
import ToolListFlatView from '../tool/tool-list-flat-view/list'
import ToolListTreeView from '../tool/tool-list-tree-view/list'
import { ViewType } from '../view-type-select'
@@ -27,6 +30,7 @@ const List = ({
className,
}: ListProps) => {
const language = useGetLanguage()
const previewCardHandle = useMemo(() => createPreviewCardHandle<ToolActionPreviewPayload>(), [])
const isFlatView = viewType === ViewType.flat
const { letters, groups: withLetterAndGroupViewToolsData } = groupItems(tools, tool => tool.label[language]![0]!)
@@ -58,7 +62,7 @@ const List = ({
return result
}, [withLetterAndGroupViewToolsData, letters])
const toolRefs = useRef({})
const toolRefsRef = useRef<Record<string, HTMLDivElement | null>>({})
const handleSelect = useCallback((type: BlockEnum, tool: ToolDefaultValue) => {
onSelect(type, tool)
@@ -70,9 +74,10 @@ const List = ({
isFlatView
? (
<ToolListFlatView
toolRefs={toolRefs}
toolRefs={toolRefsRef}
letters={letters}
payload={listViewToolData}
previewCardHandle={previewCardHandle}
isShowLetterIndex={false}
hasSearchText={false}
onSelect={handleSelect}
@@ -83,12 +88,18 @@ const List = ({
: (
<ToolListTreeView
payload={treeViewToolsData}
previewCardHandle={previewCardHandle}
hasSearchText={false}
onSelect={handleSelect}
canNotSelectMultiple
/>
)
)}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<ToolActionPreviewCard payload={payload as ToolActionPreviewPayload | undefined} />
)}
</PreviewCard>
{
unInstalledPlugins.map((item) => {
return (

View File

@@ -1,6 +1,7 @@
import type { BlockEnum, CommonNodeType } from '../types'
import type { TriggerDefaultValue } from './types'
import {
createPreviewCardHandle,
PreviewCard,
PreviewCardContent,
PreviewCardTrigger,
@@ -25,6 +26,9 @@ type StartBlocksProps = {
onContentStateChange?: (hasContent: boolean) => void
hideUserInput?: boolean
}
type StartBlockPreviewPayload = {
block: typeof START_BLOCKS[number]
}
const StartBlocks = ({
searchText,
@@ -35,6 +39,7 @@ const StartBlocks = ({
}: StartBlocksProps) => {
const { t } = useTranslation()
const nodes = useNodes()
const previewCardHandle = useMemo(() => createPreviewCardHandle<StartBlockPreviewPayload>(), [])
// const nodeMetaData = useNodeMetaData()
const filteredBlocks = useMemo(() => {
@@ -74,54 +79,31 @@ const StartBlocks = ({
// the start node, so hover/focus-only activation is a11y-safe. See
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => (
<PreviewCard key={block.type}>
<PreviewCardTrigger
delay={150}
closeDelay={150}
render={(
<div
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
onClick={() => onSelect(block.type)}
>
<BlockIcon
className="mr-2 shrink-0"
type={block.type}
/>
<div className="flex w-0 grow items-center justify-between text-sm text-text-secondary">
<span className="truncate">{t(`blocks.${block.type}`, { ns: 'workflow' })}</span>
{block.type === BlockEnumValues.Start && (
<span className="ml-2 shrink-0 system-xs-regular text-text-quaternary">{t('blocks.originalStartNode', { ns: 'workflow' })}</span>
)}
</div>
</div>
)}
/>
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
<div>
<PreviewCardTrigger
key={block.type}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ block }}
render={(
<div
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
onClick={() => onSelect(block.type)}
>
<BlockIcon
size="md"
className="mb-2"
className="mr-2 shrink-0"
type={block.type}
/>
<div className="mb-1 system-md-medium text-text-primary">
{block.type === BlockEnumValues.TriggerWebhook
? t('customWebhook', { ns: 'workflow' })
: t(`blocks.${block.type}`, { ns: 'workflow' })}
<div className="flex w-0 grow items-center justify-between text-sm text-text-secondary">
<span className="truncate">{t(`blocks.${block.type}`, { ns: 'workflow' })}</span>
{block.type === BlockEnumValues.Start && (
<span className="ml-2 shrink-0 system-xs-regular text-text-quaternary">{t('blocks.originalStartNode', { ns: 'workflow' })}</span>
)}
</div>
<div className="system-xs-regular text-text-secondary">
{t(`blocksAbout.${block.type}`, { ns: 'workflow' })}
</div>
{(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && (
<div className="mt-1 mb-1 system-xs-regular text-text-tertiary">
{t('author', { ns: 'tools' })}
{' '}
{t('difyTeam', { ns: 'workflow' })}
</div>
)}
</div>
</PreviewCardContent>
</PreviewCard>
), [onSelect, t])
)}
/>
), [onSelect, previewCardHandle, t])
if (isEmpty)
return null
@@ -140,8 +122,58 @@ const StartBlocks = ({
</div>
))}
</div>
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<StartBlockPreviewCard
payload={payload as StartBlockPreviewPayload | undefined}
t={t}
/>
)}
</PreviewCard>
</div>
)
}
type StartBlockPreviewCardProps = {
payload?: StartBlockPreviewPayload
t: ReturnType<typeof useTranslation>['t']
}
function StartBlockPreviewCard({
payload,
t,
}: StartBlockPreviewCardProps) {
if (!payload)
return null
const { block } = payload
return (
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
<div>
<BlockIcon
size="md"
className="mb-2"
type={block.type}
/>
<div className="mb-1 system-md-medium text-text-primary">
{block.type === BlockEnumValues.TriggerWebhook
? t('customWebhook', { ns: 'workflow' })
: t(`blocks.${block.type}`, { ns: 'workflow' })}
</div>
<div className="system-xs-regular wrap-break-word text-text-secondary">
{t(`blocksAbout.${block.type}`, { ns: 'workflow' })}
</div>
{(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && (
<div className="mt-1 mb-1 system-xs-regular text-text-tertiary">
{t('author', { ns: 'tools' })}
{' '}
{t('difyTeam', { ns: 'workflow' })}
</div>
)}
</div>
</PreviewCardContent>
)
}
export default memo(StartBlocks)

View File

@@ -1,3 +1,4 @@
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { trackEvent } from '@/app/components/base/amplitude'
@@ -51,6 +52,7 @@ describe('Tool', () => {
createTool('tool-b', 'Tool B'),
],
})}
previewCardHandle={createPreviewCardHandle()}
viewType={ViewType.flat}
hasSearchText={false}
onSelect={onSelect}
@@ -82,6 +84,7 @@ describe('Tool', () => {
type: CollectionType.workflow,
tools: [createTool('workflow-tool', 'Workflow Tool')],
})}
previewCardHandle={createPreviewCardHandle()}
viewType={ViewType.flat}
hasSearchText={false}
onSelect={onSelect}

View File

@@ -1,10 +1,10 @@
'use client'
import type { FC } from 'react'
import type { ComponentProps, FC } from 'react'
import type { ToolWithProvider } from '../../types'
import type { ToolDefaultValue } from '../types'
import type { Tool } from '@/app/components/tools/types'
import { cn } from '@langgenius/dify-ui/cn'
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -27,14 +27,25 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
type Props = {
provider: ToolWithProvider
payload: Tool
previewCardHandle: PreviewCardHandle
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
}
export type ToolActionPreviewPayload = {
providerIcon: ToolWithProvider['icon']
payload: Tool
language: ReturnType<typeof useGetLanguage>
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
export type ToolActionPreviewCardHandle = PreviewCardHandle
const ToolItem: FC<Props> = ({
provider,
payload,
previewCardHandle,
onSelect,
disabled,
isAdded,
@@ -107,21 +118,45 @@ const ToolItem: FC<Props> = ({
// reachable from the node inspector after the row is clicked to add the tool,
// so hover/focus-only activation is a11y-safe. See
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
<PreviewCard key={payload.name}>
<PreviewCardTrigger delay={150} closeDelay={150} render={row} />
<PreviewCardContent placement="right" popupClassName="w-[200px] px-3 py-2.5">
<div>
<BlockIcon
size="md"
className="mb-2"
type={BlockEnum.Tool}
toolIcon={providerIcon}
/>
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label[language]}</div>
<div className="text-xs leading-[18px] text-text-secondary">{payload.description[language]}</div>
</div>
</PreviewCardContent>
</PreviewCard>
<PreviewCardTrigger
key={payload.name}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{
providerIcon,
payload,
language,
}}
render={row}
/>
)
}
type ToolActionPreviewCardProps = {
payload?: ToolActionPreviewPayload
}
export function ToolActionPreviewCard({
payload,
}: ToolActionPreviewCardProps) {
if (!payload)
return null
return (
<PreviewCardContent placement="right" popupClassName="w-[200px] px-3 py-2.5">
<div>
<BlockIcon
size="md"
className="mb-2"
type={BlockEnum.Tool}
toolIcon={payload.providerIcon}
/>
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.payload.label[payload.language]}</div>
<div className="text-xs leading-[18px] wrap-break-word text-text-secondary">{payload.payload.description[payload.language]}</div>
</div>
</PreviewCardContent>
)
}
export default React.memo(ToolItem)

View File

@@ -1,3 +1,4 @@
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import { render, screen } from '@testing-library/react'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
@@ -37,6 +38,7 @@ describe('ToolListFlatView', () => {
render(
<List
letters={['A', 'B']}
previewCardHandle={createPreviewCardHandle()}
payload={[
createToolProvider({
id: 'provider-a',

View File

@@ -1,7 +1,8 @@
'use client'
import type { FC } from 'react'
import type { FC, RefObject } from 'react'
import type { BlockEnum, ToolWithProvider } from '../../../types'
import type { ToolDefaultValue, ToolValue } from '../../types'
import type { ToolActionPreviewCardHandle } from '../action-item'
import * as React from 'react'
import { useMemo } from 'react'
import { ViewType } from '../../view-type-select'
@@ -9,6 +10,7 @@ import Tool from '../tool'
type Props = {
payload: ToolWithProvider[]
previewCardHandle: ToolActionPreviewCardHandle
isShowLetterIndex: boolean
indexBar: React.ReactNode
hasSearchText: boolean
@@ -16,13 +18,14 @@ type Props = {
canNotSelectMultiple?: boolean
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
letters: string[]
toolRefs: any
toolRefs: RefObject<Record<string, HTMLDivElement | null>>
selectedTools?: ToolValue[]
}
const ToolViewFlatView: FC<Props> = ({
letters,
payload,
previewCardHandle,
isShowLetterIndex,
indexBar,
hasSearchText,
@@ -55,6 +58,7 @@ const ToolViewFlatView: FC<Props> = ({
>
<Tool
payload={tool}
previewCardHandle={previewCardHandle}
viewType={ViewType.flat}
hasSearchText={hasSearchText}
onSelect={onSelect}

View File

@@ -1,3 +1,4 @@
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import { render, screen } from '@testing-library/react'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
@@ -36,6 +37,7 @@ describe('ToolListTreeView Item', () => {
toolList={[createToolProvider({
label: { en_US: 'Provider Alpha', zh_Hans: 'Provider Alpha' },
})]}
previewCardHandle={createPreviewCardHandle()}
hasSearchText={false}
onSelect={vi.fn()}
/>,

View File

@@ -1,3 +1,4 @@
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
import { render, screen } from '@testing-library/react'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
@@ -43,6 +44,7 @@ describe('ToolListTreeView', () => {
label: { en_US: 'Custom Provider', zh_Hans: 'Custom Provider' },
})],
}}
previewCardHandle={createPreviewCardHandle()}
hasSearchText={false}
onSelect={vi.fn()}
/>,

View File

@@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { BlockEnum, ToolWithProvider } from '../../../types'
import type { ToolDefaultValue, ToolValue } from '../../types'
import type { ToolActionPreviewCardHandle } from '../action-item'
import * as React from 'react'
import { ViewType } from '../../view-type-select'
import Tool from '../tool'
@@ -9,6 +10,7 @@ import Tool from '../tool'
type Props = {
groupName: string
toolList: ToolWithProvider[]
previewCardHandle: ToolActionPreviewCardHandle
hasSearchText: boolean
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
@@ -19,6 +21,7 @@ type Props = {
const Item: FC<Props> = ({
groupName,
toolList,
previewCardHandle,
hasSearchText,
onSelect,
canNotSelectMultiple,
@@ -35,6 +38,7 @@ const Item: FC<Props> = ({
<Tool
key={tool.id}
payload={tool}
previewCardHandle={previewCardHandle}
viewType={ViewType.tree}
hasSearchText={hasSearchText}
onSelect={onSelect}

View File

@@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { BlockEnum, ToolWithProvider } from '../../../types'
import type { ToolDefaultValue, ToolValue } from '../../types'
import type { ToolActionPreviewCardHandle } from '../action-item'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
@@ -10,6 +11,7 @@ import Item from './item'
type Props = {
payload: Record<string, ToolWithProvider[]>
previewCardHandle: ToolActionPreviewCardHandle
hasSearchText: boolean
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
canNotSelectMultiple?: boolean
@@ -19,6 +21,7 @@ type Props = {
const ToolListTreeView: FC<Props> = ({
payload,
previewCardHandle,
hasSearchText,
onSelect,
canNotSelectMultiple,
@@ -49,6 +52,7 @@ const ToolListTreeView: FC<Props> = ({
key={groupName}
groupName={getI18nGroupName(groupName)}
toolList={payload[groupName]!}
previewCardHandle={previewCardHandle}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}

View File

@@ -3,11 +3,12 @@ import type { FC } from 'react'
import type { Tool as ToolType } from '../../../tools/types'
import type { ToolWithProvider } from '../../types'
import type { ToolDefaultValue, ToolValue } from '../types'
import type { ToolActionPreviewCardHandle } from './action-item'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useHover } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useCallback, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Mcp } from '@/app/components/base/icons/src/vender/other'
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
@@ -33,6 +34,7 @@ const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
type Props = {
className?: string
payload: ToolWithProvider
previewCardHandle: ToolActionPreviewCardHandle
viewType: ViewType
hasSearchText: boolean
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
@@ -45,6 +47,7 @@ type Props = {
const Tool: FC<Props> = ({
className,
payload,
previewCardHandle,
viewType,
hasSearchText,
onSelect,
@@ -59,7 +62,8 @@ const Tool: FC<Props> = ({
const notShowProvider = payload.type === CollectionType.workflow
const actions = payload.tools
const hasAction = !notShowProvider
const [isFold, setFold] = React.useState<boolean>(true)
const [isFold, setIsFold] = React.useState<boolean>(true)
const [isFoldHasSearchText, setIsFoldHasSearchText] = React.useState(hasSearchText)
const ref = useRef(null)
const isHovering = useHover(ref)
const isMCPTool = payload.type === CollectionType.mcp
@@ -146,14 +150,10 @@ const Tool: FC<Props> = ({
)
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])
useEffect(() => {
if (hasSearchText && isFold) {
setFold(false)
return
}
if (!hasSearchText && !isFold)
setFold(true)
}, [hasSearchText])
if (isFoldHasSearchText !== hasSearchText) {
setIsFoldHasSearchText(hasSearchText)
setIsFold(!hasSearchText)
}
const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine
@@ -181,7 +181,7 @@ const Tool: FC<Props> = ({
className="group/item flex w-full cursor-pointer items-center justify-between rounded-lg pr-1 pl-3 select-none hover:bg-state-base-hover"
onClick={() => {
if (hasAction) {
setFold(!isFold)
setIsFold(!isFold)
return
}
@@ -240,6 +240,7 @@ const Tool: FC<Props> = ({
key={action.name}
provider={payload}
payload={action}
previewCardHandle={previewCardHandle}
onSelect={onSelect}
disabled={getIsDisabled(action) || isShowCanNotChooseMCPTip}
isAdded={getIsDisabled(action)}

View File

@@ -1,10 +1,13 @@
import type { BlockEnum, ToolWithProvider } from '../types'
import type { ToolActionPreviewPayload } from './tool/action-item'
import type { ToolDefaultValue, ToolTypeEnum, ToolValue } from './types'
import { cn } from '@langgenius/dify-ui/cn'
import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card'
import { memo, useMemo, useRef } from 'react'
import Empty from '@/app/components/tools/provider/empty'
import { useGetLanguage } from '@/context/i18n'
import IndexBar, { groupItems } from './index-bar'
import { ToolActionPreviewCard } from './tool/action-item'
import ToolListFlatView from './tool/tool-list-flat-view/list'
import ToolListTreeView from './tool/tool-list-tree-view/list'
import { ViewType } from './view-type-select'
@@ -35,8 +38,8 @@ const Tools = ({
indexBarClassName,
selectedTools,
}: ToolsProps) => {
// const tools: any = []
const language = useGetLanguage()
const previewCardHandle = useMemo(() => createPreviewCardHandle<ToolActionPreviewPayload>(), [])
const isFlatView = viewType === ViewType.flat
const isShowLetterIndex = isFlatView && tools.length > 10
@@ -85,7 +88,7 @@ const Tools = ({
return result
}, [withLetterAndGroupViewToolsData, letters])
const toolRefs = useRef({})
const toolRefsRef = useRef<Record<string, HTMLDivElement | null>>({})
return (
<div className={cn('max-w-full p-1', className)}>
@@ -98,21 +101,23 @@ const Tools = ({
isFlatView
? (
<ToolListFlatView
toolRefs={toolRefs}
toolRefs={toolRefsRef}
letters={letters}
payload={listViewToolData}
previewCardHandle={previewCardHandle}
isShowLetterIndex={isShowLetterIndex}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
onSelectMultiple={onSelectMultiple}
selectedTools={selectedTools}
indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
indexBar={<IndexBar letters={letters} itemRefs={toolRefsRef} className={indexBarClassName} />}
/>
)
: (
<ToolListTreeView
payload={treeViewToolsData}
previewCardHandle={previewCardHandle}
hasSearchText={hasSearchText}
onSelect={onSelect}
canNotSelectMultiple={canNotSelectMultiple}
@@ -121,6 +126,11 @@ const Tools = ({
/>
)
)}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<ToolActionPreviewCard payload={payload as ToolActionPreviewPayload | undefined} />
)}
</PreviewCard>
</div>
)
}

View File

@@ -1,9 +1,9 @@
'use client'
import type { FC } from 'react'
import type { ComponentProps, FC } from 'react'
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
import type { Event } from '@/app/components/tools/types'
import { cn } from '@langgenius/dify-ui/cn'
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import { PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useGetLanguage } from '@/context/i18n'
@@ -13,14 +13,25 @@ import { BlockEnum } from '../../types'
type Props = {
provider: TriggerWithProvider
payload: Event
previewCardHandle: TriggerPluginActionPreviewCardHandle
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
}
export type TriggerPluginActionPreviewPayload = {
provider: TriggerWithProvider
payload: Event
language: ReturnType<typeof useGetLanguage>
}
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
export type TriggerPluginActionPreviewCardHandle = PreviewCardHandle
const TriggerPluginActionItem: FC<Props> = ({
provider,
payload,
previewCardHandle,
onSelect,
disabled,
isAdded,
@@ -37,7 +48,7 @@ const TriggerPluginActionItem: FC<Props> = ({
return
const params: Record<string, string> = {}
if (payload.parameters) {
payload.parameters.forEach((item: any) => {
payload.parameters.forEach((item) => {
params[item.name] = ''
})
}
@@ -73,21 +84,41 @@ const TriggerPluginActionItem: FC<Props> = ({
// reachable from the node inspector after the row is clicked to add the trigger,
// so hover/focus-only activation is a11y-safe. See
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
<PreviewCard key={payload.name}>
<PreviewCardTrigger delay={150} closeDelay={150} render={row} />
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
<div>
<BlockIcon
size="md"
className="mb-2"
type={BlockEnum.TriggerPlugin}
toolIcon={provider.icon}
/>
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label[language]}</div>
<div className="text-xs leading-[18px] text-text-secondary">{payload.description[language]}</div>
</div>
</PreviewCardContent>
</PreviewCard>
<PreviewCardTrigger
key={payload.name}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ provider, payload, language }}
render={row}
/>
)
}
type TriggerPluginActionPreviewCardProps = {
payload?: TriggerPluginActionPreviewPayload
}
export function TriggerPluginActionPreviewCard({
payload,
}: TriggerPluginActionPreviewCardProps) {
if (!payload)
return null
return (
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 py-2.5">
<div>
<BlockIcon
size="md"
className="mb-2"
type={BlockEnum.TriggerPlugin}
toolIcon={payload.provider.icon}
/>
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.payload.label[payload.language]}</div>
<div className="text-xs leading-[18px] wrap-break-word text-text-secondary">{payload.payload.description[payload.language]}</div>
</div>
</PreviewCardContent>
)
}
export default React.memo(TriggerPluginActionItem)

View File

@@ -1,10 +1,11 @@
'use client'
import type { FC } from 'react'
import type { TriggerPluginActionPreviewCardHandle } from './action-item'
import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useMemo, useRef } from 'react'
import { useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { CollectionType } from '@/app/components/tools/types'
import BlockIcon from '@/app/components/workflow/block-icon'
@@ -27,6 +28,7 @@ type Props = {
className?: string
payload: TriggerWithProvider
hasSearchText: boolean
previewCardHandle: TriggerPluginActionPreviewCardHandle
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
}
@@ -34,6 +36,7 @@ const TriggerPluginItem: FC<Props> = ({
className,
payload,
hasSearchText,
previewCardHandle,
onSelect,
}) => {
const { t } = useTranslation()
@@ -42,17 +45,14 @@ const TriggerPluginItem: FC<Props> = ({
const notShowProvider = payload.type === CollectionType.workflow
const actions = payload.events
const hasAction = !notShowProvider
const [isFold, setFold] = React.useState<boolean>(true)
const [isFold, setIsFold] = React.useState<boolean>(true)
const [isFoldHasSearchText, setIsFoldHasSearchText] = React.useState(hasSearchText)
const ref = useRef(null)
useEffect(() => {
if (hasSearchText && isFold) {
setFold(false)
return
}
if (!hasSearchText && !isFold)
setFold(true)
}, [hasSearchText])
if (isFoldHasSearchText !== hasSearchText) {
setIsFoldHasSearchText(hasSearchText)
setIsFold(!hasSearchText)
}
const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine
@@ -97,14 +97,14 @@ const TriggerPluginItem: FC<Props> = ({
className="group/item flex w-full cursor-pointer items-center justify-between rounded-lg pr-1 pl-3 select-none hover:bg-state-base-hover"
onClick={() => {
if (hasAction) {
setFold(!isFold)
setIsFold(!isFold)
return
}
const event = actions[0]
const params: Record<string, string> = {}
if (event!.parameters) {
event!.parameters.forEach((item: any) => {
event!.parameters.forEach((item) => {
params[item.name] = ''
})
}
@@ -150,6 +150,7 @@ const TriggerPluginItem: FC<Props> = ({
key={action.name}
provider={providerWithResolvedIcon}
payload={action}
previewCardHandle={previewCardHandle}
onSelect={onSelect}
disabled={false}
isAdded={false}

View File

@@ -1,9 +1,12 @@
'use client'
import type { BlockEnum } from '../../types'
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
import type { TriggerPluginActionPreviewPayload } from './action-item'
import { createPreviewCardHandle, PreviewCard } from '@langgenius/dify-ui/preview-card'
import { memo, useEffect, useMemo } from 'react'
import { useGetLanguage } from '@/context/i18n'
import { useAllTriggerPlugins } from '@/service/use-triggers'
import { TriggerPluginActionPreviewCard } from './action-item'
import TriggerPluginItem from './item'
type TriggerPluginListProps = {
@@ -20,6 +23,7 @@ const TriggerPluginList = ({
}: TriggerPluginListProps) => {
const { data: triggerPluginsData } = useAllTriggerPlugins()
const language = useGetLanguage()
const previewCardHandle = useMemo(() => createPreviewCardHandle<TriggerPluginActionPreviewPayload>(), [])
const normalizedSearch = searchText.trim().toLowerCase()
const triggerPlugins = useMemo(() => {
@@ -96,8 +100,14 @@ const TriggerPluginList = ({
payload={plugin}
onSelect={onSelect}
hasSearchText={!!searchText}
previewCardHandle={previewCardHandle}
/>
))}
<PreviewCard handle={previewCardHandle}>
{({ payload }) => (
<TriggerPluginActionPreviewCard payload={payload as TriggerPluginActionPreviewPayload | undefined} />
)}
</PreviewCard>
</div>
)
}

View File

@@ -101,8 +101,8 @@ const FormContent: FC<FormContentProps> = ({
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
width: node.width ?? undefined,
height: node.height ?? undefined,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {

View File

@@ -1,7 +1,7 @@
import type { DocPathMap } from './i18n'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { useTranslation } from '#i18n'
import { renderHook } from '@testing-library/react'
import { useTranslation } from '#i18n'
import { getDocLanguage } from '@/i18n-config/language'
import { defaultDocBaseUrl, useDocLink } from './i18n'

View File

@@ -1,7 +1,7 @@
import type { Locale } from '@/i18n-config/language'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { useTranslation } from '#i18n'
import { useCallback } from 'react'
import { useTranslation } from '#i18n'
import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n-config/language'
import { apiReferencePathTranslations } from '@/types/doc-paths'

View File

@@ -56,7 +56,6 @@ export default antfu(
{
files: [...GLOB_TESTS, GLOB_MARKDOWN_CODE, 'vitest.setup.ts', 'test/i18n-mock.ts'],
rules: {
'react/component-hook-factories': 'off',
'react/no-unnecessary-use-prefix': 'off',
},
},
@@ -157,12 +156,6 @@ export default antfu(
'dify/consistent-placeholders': 'error',
},
},
{
files: ['package.json'],
rules: {
'hyoban/no-dependency-version-prefix': 'error',
},
},
{
name: 'dify/restricted-imports',
files: [GLOB_TS, GLOB_TSX],