Compare commits

..

3 Commits

Author SHA1 Message Date
449ea15941 fix(workflow): reset block selector tab on close 2026-06-15 13:20:04 -07:00
de2ec990d8 chore: example to color the session (#37402)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-15 18:06:24 +00:00
2b7f5ab982 fix: project agent node outputs into draft graph (#37467) 2026-06-15 13:27:57 +00:00
280 changed files with 901 additions and 19304 deletions

View File

@ -44,21 +44,3 @@ The codebase is split into:
- Backend architecture adheres to DDD and Clean Architecture principles.
- Async work runs through Celery with Redis as the broker.
- Frontend user-facing strings must use `web/i18n/en-US/`; avoid hardcoded text.
## Agent V2 Frontend Constraints
- Treat workflow Agent and Agent v2 as separate product surfaces. The existing workflow `agent` node is the legacy Old Agent and should keep its current strategy/plugin-based behavior. Do not refactor legacy Agent code to support Agent v2 unless explicitly requested.
- New Agent work must use the Agent v2 surface and code path: `web/features/agent-v2`, `web/app/components/workflow/nodes/agent-v2`, `BlockEnum.AgentV2`, and the `agentV2` i18n namespace where applicable. Do not add new Agent behavior to legacy `web/app/components/workflow/nodes/agent`.
- Do not mix, alias, or compatibility-bridge Old Agent and Agent v2 data shapes. Keep fields such as `agent_strategy_*` on legacy Agent only, and fields such as `agent_roster`, `agent_task`, and Agent v2 backend bindings on Agent v2 only.
- Shared workflow utilities may branch on the explicit node type/discriminator when necessary, but they must preserve the boundary: legacy Agent behavior must not depend on Agent v2 data, and Agent v2 behavior must not fall back to legacy Agent strategy/plugin behavior.
- For Agent v2 frontend work under `web/features/agent-v2`, use generated contracts and `consoleQuery` from `@/service/client` for all Agent v2 backend APIs. Do not add ad hoc REST helpers, mock data, compatibility shims, or handwritten API types for new Agent v2 interfaces.
- Keep Agent v2 composer state split by responsibility: TanStack Query is the server source of truth, Jotai atoms hold only the editable composer draft, and local component state owns transient UI such as menus, dialogs, and expanded panels.
- Wrap editable Agent v2 composer surfaces in an instance-level `AgentComposerProvider`. The provider is an editing-session boundary, not a data-fetching layer; use one store per agent/configure page or workflow node composer instance to avoid draft leakage.
- Hydrate composer atoms from generated contract data by mapping the response into both `originalDraft` and `draft`. Compute dirty state from `draft` vs `originalDraft`; do not compare editable draft directly against live TanStack Query cache.
- Keep mock or transitional composer defaults at the owning surface boundary, such as the configure page. Do not put page-specific mock data into shared `agent-composer` store defaults.
- Use existing `@langgenius/dify-ui/*` primitives before adding feature-local UI chrome. Prefer primitive default styles; add call-site CSS only for real design deltas.
- Prefer primitive data/CSS selectors for visual states instead of mirroring state in React only to choose classes.
- Avoid arbitrary Tailwind values when an existing project utility or token class expresses the same value. Keep arbitrary values only for design-system exceptions without a native utility.
- Preserve keyboard accessibility in Agent v2 pages: visible focus rings must not be clipped, and inert layout regions should not become keyboard focus targets.
- Keep Agent v2 i18n scoped to the explicitly maintained locales unless the supported-locale scope changes.
- Keep Agent v2 module copy in the `agentV2` namespace; use shared namespaces such as `common` only for genuinely shared operation labels.

View File

@ -2,12 +2,12 @@ import json
import logging
from collections.abc import Sequence
from datetime import datetime
from typing import Any, NotRequired, TypedDict
from typing import Any, NotRequired, TypedDict, cast
from flask import abort, request
from flask_restx import Resource, fields
from pydantic import AliasChoices, BaseModel, Field, RootModel, ValidationError, field_validator
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import Session, sessionmaker
from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound
import services
@ -449,8 +449,16 @@ class DraftWorkflowApi(Resource):
if not workflow:
raise DraftWorkflowNotExist()
# return workflow, if not found, return 404
return dump_response(WorkflowResponse, workflow)
from services.agent.workflow_publish_service import WorkflowAgentPublishService
# Return workflow with response-only Agent node job projection so the
# front-end can treat draft graph node data as the editing source.
response = WorkflowResponse.model_validate(workflow, from_attributes=True).model_dump(mode="json")
response["graph"] = WorkflowAgentPublishService.project_draft_bindings_to_graph(
session=cast(Session, db.session),
draft_workflow=workflow,
)
return response
@setup_required
@login_required

View File

@ -5,6 +5,8 @@ import re
import uuid
from typing import Any, TypedDict, cast, override
from sqlalchemy.orm import scoped_session
logger = logging.getLogger(__name__)
from sqlalchemy import select
@ -411,11 +413,13 @@ class ParagraphIndexProcessor(BaseIndexProcessor):
if supports_vision:
# First, try to get images from SegmentAttachmentBinding (preferred method)
if segment_id:
image_files = ParagraphIndexProcessor._extract_images_from_segment_attachments(tenant_id, segment_id)
image_files = ParagraphIndexProcessor._extract_images_from_segment_attachments(
tenant_id, segment_id, db.session
)
# If no images from attachments, fall back to extracting from text
if not image_files:
image_files = ParagraphIndexProcessor._extract_images_from_text(tenant_id, text)
image_files = ParagraphIndexProcessor._extract_images_from_text(tenant_id, text, db.session)
# Build prompt messages
prompt_messages = []
@ -469,7 +473,7 @@ class ParagraphIndexProcessor(BaseIndexProcessor):
return summary_content, usage
@staticmethod
def _extract_images_from_text(tenant_id: str, text: str) -> list[File]:
def _extract_images_from_text(tenant_id: str, text: str, session: scoped_session) -> list[File]:
"""
Extract images from markdown text and convert them to File objects.
@ -518,7 +522,7 @@ class ParagraphIndexProcessor(BaseIndexProcessor):
# Get unique IDs for database query
unique_upload_file_ids = list(set(upload_file_id_list))
upload_files = db.session.scalars(
upload_files = session.scalars(
select(UploadFile).where(UploadFile.id.in_(unique_upload_file_ids), UploadFile.tenant_id == tenant_id)
).all()
@ -549,7 +553,9 @@ class ParagraphIndexProcessor(BaseIndexProcessor):
return file_objects
@staticmethod
def _extract_images_from_segment_attachments(tenant_id: str, segment_id: str) -> list[File]:
def _extract_images_from_segment_attachments(
tenant_id: str, segment_id: str, session: scoped_session
) -> list[File]:
"""
Extract images from SegmentAttachmentBinding table (preferred method).
This matches how DatasetRetrieval gets segment attachments.
@ -564,7 +570,7 @@ class ParagraphIndexProcessor(BaseIndexProcessor):
from sqlalchemy import select
# Query attachments from SegmentAttachmentBinding table
attachments_with_bindings = db.session.execute(
attachments_with_bindings = session.execute(
select(SegmentAttachmentBinding, UploadFile)
.join(UploadFile, UploadFile.id == SegmentAttachmentBinding.attachment_id)
.where(

View File

@ -1,7 +1,7 @@
"""add identity mode to mcp tool provider
Revision ID: 3df4dbcc1e21
Revises: c4d5e6f7a8b9
Revises: 2b3c4d5e6f70
Create Date: 2026-05-29 15:00:00.000000
Adds the `identity_mode` column to `tool_mcp_providers` to drive the M2 MCP
@ -23,7 +23,7 @@ import models as models
# revision identifiers, used by Alembic.
revision = "3df4dbcc1e21"
down_revision = "c4d5e6f7a8b9"
down_revision = "2b3c4d5e6f70"
branch_labels = None
depends_on = None

View File

@ -1,7 +1,8 @@
from __future__ import annotations
import copy
from collections.abc import Mapping
from typing import Any
from typing import Any, cast
from pydantic import ValidationError
from sqlalchemy import select
@ -21,6 +22,41 @@ class WorkflowAgentPublishService:
_AGENT_TASK_KEY = "agent_task"
_AGENT_DECLARED_OUTPUTS_KEY = "agent_declared_outputs"
@classmethod
def project_draft_bindings_to_graph(cls, *, session: Session, draft_workflow: Workflow) -> dict[str, Any]:
"""Return draft graph with persisted Agent node job config projected into node data.
Workflow draft graph is the front-end's editing source of truth, while
runtime/publish reads WorkflowAgentNodeBinding.node_job_config. This
response-only projection keeps reads aligned without writing binding
details back into the stored graph JSON.
"""
graph = cast(dict[str, Any], copy.deepcopy(draft_workflow.graph_dict))
agent_nodes = dict(WorkflowAgentNodeValidator.iter_agent_v2_nodes(graph))
if not agent_nodes:
return graph
bindings = session.scalars(
select(WorkflowAgentNodeBinding).where(
WorkflowAgentNodeBinding.tenant_id == draft_workflow.tenant_id,
WorkflowAgentNodeBinding.app_id == draft_workflow.app_id,
WorkflowAgentNodeBinding.workflow_id == draft_workflow.id,
WorkflowAgentNodeBinding.workflow_version == cls._DRAFT_WORKFLOW_VERSION,
WorkflowAgentNodeBinding.node_id.in_(list(agent_nodes.keys())),
)
).all()
for binding in bindings:
node_data = agent_nodes.get(binding.node_id)
if not isinstance(node_data, dict):
continue
node_job = WorkflowNodeJobConfig.model_validate(binding.node_job_config_dict)
if node_job.workflow_prompt is not None:
node_data[cls._AGENT_TASK_KEY] = node_job.workflow_prompt
node_data[cls._AGENT_DECLARED_OUTPUTS_KEY] = [
output.model_dump(mode="json") for output in node_job.declared_outputs
]
return graph
@classmethod
def validate_agent_nodes_for_publish(cls, *, session: Session, draft_workflow: Workflow) -> None:
WorkflowAgentNodeValidator.validate_published_workflow(session=session, workflow=draft_workflow)

View File

@ -60,7 +60,6 @@ class ComposerSavePayload(BaseModel):
class RosterAgentCreatePayload(BaseModel):
name: str = Field(min_length=1, max_length=255)
mode: Literal["agent"] = "agent"
description: str = ""
role: str = Field(default="", max_length=255)
icon_type: AgentIconType | None = None

View File

@ -540,6 +540,58 @@ def test_draft_workflow_get_not_found(monkeypatch: pytest.MonkeyPatch) -> None:
handler(api, app_model=SimpleNamespace(id="app"))
def test_draft_workflow_get_projects_agent_node_job_to_graph(monkeypatch: pytest.MonkeyPatch) -> None:
workflow = _make_workflow(
graph_dict={
"nodes": [
{
"id": "agent-node",
"data": {
"type": "agent",
"version": "2",
},
}
],
"edges": [],
}
)
projected_graph = {
"nodes": [
{
"id": "agent-node",
"data": {
"type": "agent",
"version": "2",
"agent_task": "Summarize it.",
"agent_declared_outputs": [{"name": "summary", "type": "string"}],
},
}
],
"edges": [],
}
monkeypatch.setattr(
workflow_module,
"WorkflowService",
lambda: SimpleNamespace(get_draft_workflow=lambda **_k: workflow),
)
from services.agent.workflow_publish_service import WorkflowAgentPublishService
monkeypatch.setattr(
WorkflowAgentPublishService,
"project_draft_bindings_to_graph",
lambda **_k: projected_graph,
)
api = workflow_module.DraftWorkflowApi()
handler = inspect.unwrap(api.get)
response = handler(api, app_model=SimpleNamespace(id="app"))
assert response["graph"] == projected_graph
def test_advanced_chat_run_conversation_not_exists(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
workflow_module.AppGenerateService,

View File

@ -528,21 +528,24 @@ class TestParagraphIndexProcessor:
session.scalars.return_value = scalars_result
with (
patch("core.rag.index_processor.processor.paragraph_index_processor.db.session", session),
patch(
"core.rag.index_processor.processor.paragraph_index_processor.build_from_mapping",
return_value=SimpleNamespace(id="file-1"),
) as mock_builder,
patch("core.rag.index_processor.processor.paragraph_index_processor.logger") as mock_logger,
):
files = ParagraphIndexProcessor._extract_images_from_text("tenant-1", text)
files = ParagraphIndexProcessor._extract_images_from_text("tenant-1", text, session)
assert len(files) == 1
assert mock_builder.call_count == 1
mock_logger.warning.assert_not_called()
def test_extract_images_from_text_returns_empty_when_no_matches(self) -> None:
assert ParagraphIndexProcessor._extract_images_from_text("tenant-1", "no images here") == []
scalars_result = Mock()
scalars_result.all.return_value = []
session = Mock()
session.scalars.return_value = scalars_result
assert ParagraphIndexProcessor._extract_images_from_text("tenant-1", "no images here", session) == []
def test_extract_images_from_text_logs_when_build_fails(self) -> None:
text = "![img](/files/11111111-1111-1111-1111-111111111111/image-preview)"
@ -562,14 +565,13 @@ class TestParagraphIndexProcessor:
session.scalars.return_value = scalars_result
with (
patch("core.rag.index_processor.processor.paragraph_index_processor.db.session", session),
patch(
"core.rag.index_processor.processor.paragraph_index_processor.build_from_mapping",
side_effect=RuntimeError("build failed"),
),
patch("core.rag.index_processor.processor.paragraph_index_processor.logger") as mock_logger,
):
files = ParagraphIndexProcessor._extract_images_from_text("tenant-1", text)
files = ParagraphIndexProcessor._extract_images_from_text("tenant-1", text, session)
assert files == []
mock_logger.warning.assert_called_once()
@ -608,10 +610,9 @@ class TestParagraphIndexProcessor:
session.execute.return_value = execute_result
with (
patch("core.rag.index_processor.processor.paragraph_index_processor.db.session", session),
patch("core.rag.index_processor.processor.paragraph_index_processor.logger") as mock_logger,
):
files = ParagraphIndexProcessor._extract_images_from_segment_attachments("tenant-1", "seg-1")
files = ParagraphIndexProcessor._extract_images_from_segment_attachments("tenant-1", "seg-1", session)
assert len(files) == 1
mock_logger.warning.assert_called_once()
@ -622,7 +623,6 @@ class TestParagraphIndexProcessor:
session = Mock()
session.execute.return_value = execute_result
with patch("core.rag.index_processor.processor.paragraph_index_processor.db.session", session):
empty_files = ParagraphIndexProcessor._extract_images_from_segment_attachments("tenant-1", "seg-1")
empty_files = ParagraphIndexProcessor._extract_images_from_segment_attachments("tenant-1", "seg-1", session)
assert empty_files == []

View File

@ -1140,6 +1140,62 @@ class TestListWorkflowsReferencingAppAgent:
class TestWorkflowAgentDraftBindingSync:
def test_projects_binding_declared_outputs_to_draft_graph_response(self):
workflow = Workflow(
id="workflow-1",
tenant_id="tenant-1",
app_id="app-1",
version=Workflow.VERSION_DRAFT,
graph=json.dumps(
{
"nodes": [
{
"id": "agent-node",
"data": {
"type": "agent",
"version": "2",
"agent_binding": {
"binding_type": "roster_agent",
"agent_id": "agent-1",
},
},
}
],
"edges": [],
}
),
)
binding = WorkflowAgentNodeBinding(
id="binding-1",
tenant_id="tenant-1",
app_id="app-1",
workflow_id="workflow-1",
workflow_version=Workflow.VERSION_DRAFT,
node_id="agent-node",
binding_type=WorkflowAgentBindingType.ROSTER_AGENT,
agent_id="agent-1",
current_snapshot_id="snapshot-1",
node_job_config=WorkflowNodeJobConfig(
workflow_prompt="Summarize the upstream result.",
declared_outputs=[
DeclaredOutputConfig(name="summary", type=DeclaredOutputType.STRING, description="Short summary")
],
),
)
session = FakeSession(scalars=[[binding]])
graph = WorkflowAgentPublishService.project_draft_bindings_to_graph(
session=session,
draft_workflow=workflow,
)
node_data = graph["nodes"][0]["data"]
assert node_data["agent_task"] == "Summarize the upstream result."
assert node_data["agent_declared_outputs"][0]["name"] == "summary"
assert node_data["agent_declared_outputs"][0]["type"] == "string"
assert node_data["agent_declared_outputs"][0]["description"] == "Short summary"
assert "agent_declared_outputs" not in workflow.graph_dict["nodes"][0]["data"]
def test_creates_roster_binding_from_agent_node_graph(self):
workflow = Workflow(
id="workflow-1",

View File

@ -5025,6 +5025,14 @@
"count": 1
}
},
"web/app/components/workflow/block-selector/blocks.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/workflow/block-selector/featured-tools.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@ -5709,7 +5717,7 @@
"count": 2
},
"ts/no-explicit-any": {
"count": 4
"count": 6
}
},
"web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/index.tsx": {
@ -5890,6 +5898,11 @@
"count": 5
}
},
"web/app/components/workflow/nodes/components.ts": {
"ts/no-explicit-any": {
"count": 2
}
},
"web/app/components/workflow/nodes/data-source-empty/hooks.ts": {
"ts/no-explicit-any": {
"count": 1
@ -7060,6 +7073,19 @@
"count": 1
}
},
"web/app/components/workflow/run/agent-log/agent-log-trigger.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/workflow/run/agent-log/index.tsx": {
"no-barrel-files/no-barrel-files": {
"count": 2
}
},
"web/app/components/workflow/run/hooks.ts": {
"ts/no-explicit-any": {
"count": 1
@ -7742,6 +7768,11 @@
"count": 1
}
},
"web/service/client.ts": {
"no-restricted-imports": {
"count": 1
}
},
"web/service/common.ts": {
"no-restricted-imports": {
"count": 1

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.30314 1.54615C8.48087 1.50085 8.66708 1.49473 8.84742 1.52858C9.07685 1.57176 9.27914 1.6956 9.41968 1.77467L13.1768 3.88795C13.2961 3.955 13.4666 4.04398 13.6098 4.17701L13.6697 4.23691C13.7899 4.36839 13.8809 4.52463 13.9366 4.69394C14.0074 4.90941 13.9998 5.13856 13.9998 5.29485V9.55917C13.9998 9.70492 14.0069 9.91888 13.9444 10.1223C13.9076 10.2422 13.8527 10.3559 13.7823 10.4596L13.7068 10.5598C13.5702 10.7233 13.3869 10.8339 13.2647 10.9133L8.25171 14.1718C8.11671 14.2596 7.92272 14.396 7.69637 14.4537C7.51868 14.499 7.33292 14.5051 7.15275 14.4713C6.92315 14.4281 6.72034 14.3049 6.57984 14.2258L2.82268 12.1119C2.68658 12.0353 2.48283 11.9301 2.32984 11.7629C2.20953 11.6314 2.11852 11.475 2.06291 11.3059C1.99209 11.0903 1.99976 10.8606 1.99976 10.7044V6.44069C1.99976 6.29486 1.99262 6.08092 2.0551 5.87753L2.09807 5.7597C2.14656 5.64426 2.21216 5.53647 2.29273 5.44003L2.34611 5.38144C2.47424 5.24974 2.62775 5.15609 2.73479 5.08652L7.85979 1.75514C7.98194 1.67712 8.13342 1.58951 8.30314 1.54615ZM3.33309 10.7044C3.33309 10.7559 3.33334 10.7962 3.33374 10.8307C3.33392 10.8452 3.33411 10.8578 3.33439 10.8684C3.34356 10.8739 3.3543 10.8806 3.36695 10.8879C3.39682 10.9052 3.43208 10.9245 3.47697 10.9498L6.74064 12.7857V11.649L3.33309 9.73235V10.7044ZM8.07398 11.621V12.6972L12.5382 9.7955C12.5784 9.76933 12.6098 9.74883 12.6365 9.73105C12.6475 9.72368 12.6564 9.71639 12.6645 9.71087C12.6647 9.70125 12.6656 9.69004 12.6658 9.67701C12.6661 9.64494 12.6664 9.60721 12.6664 9.55917V8.636L8.07398 11.621ZM3.33309 8.2024L6.74064 10.1191V8.98235L3.33309 7.06568V8.2024ZM8.07398 8.95436V10.0299L12.6664 7.04485V5.96933L8.07398 8.95436ZM8.58374 2.87493L3.95288 5.8847L7.38192 7.81373L12.046 4.78183L8.76604 2.93613C8.71971 2.91007 8.68338 2.89008 8.6521 2.87298C8.63863 2.86561 8.62684 2.85931 8.61695 2.8541C8.60751 2.85987 8.59651 2.86685 8.58374 2.87493Z" fill="#155AEF"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,8 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z" fill="currentColor" />
<path d="M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z" fill="currentColor" />
<path d="M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z" fill="currentColor" />
<path d="M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z" fill="currentColor" />
<path d="M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z" fill="currentColor" />
<path d="M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z" fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,5 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z" fill="currentColor"/>
<path d="M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 693 B

View File

@ -1,3 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z" fill="#676F83" />
</svg>

Before

Width:  |  Height:  |  Size: 675 B

View File

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16" fill="none">
<path d="M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z" fill="#676F83"/>
<path d="M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z" fill="#676F83"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z" fill="#676F83"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -1,6 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9362 1.5C12.4177 1.50006 12.8411 1.81975 12.9727 2.28296L15.4651 11.061C15.5117 11.2251 15.6125 11.3687 15.7515 11.4675L16.1235 11.7319C16.3544 11.8963 16.4728 12.1769 16.4297 12.4571L15.9206 15.7669C15.8394 16.2948 15.2475 16.5708 14.7905 16.2942L12.8006 15.0893C12.6837 15.0186 12.5618 14.9546 12.4307 14.9165C12.2411 14.8615 12.0443 14.833 11.8462 14.833C10.6885 14.833 9.75 13.8945 9.75 12.7368V9.14722C9.75 8.23747 10.4875 7.5 11.3972 7.5C11.5688 7.5 11.7164 7.62113 11.7503 7.78928L12.3824 10.9483L12.4043 11.0215C12.4721 11.182 12.6457 11.2781 12.8233 11.2427C13.0009 11.2072 13.1242 11.0515 13.125 10.8772L13.1177 10.8017L12.4856 7.6428C12.3818 7.12384 11.9265 6.75002 11.3972 6.75C11.08 6.75 10.7771 6.81165 10.5 6.92359V2.93628C10.5 2.14312 11.1431 1.5 11.9362 1.5Z" fill="currentColor"/>
<path d="M2.28761 11.1211C2.263 11.2855 2.25026 11.4538 2.25026 11.625C2.25026 13.3862 3.59948 14.8313 5.32057 14.9854L3.0757 16.3674C2.65801 16.6245 2.10961 16.418 1.96534 15.9492L0.926773 12.5742C0.823558 12.2388 0.967018 11.876 1.27174 11.7019L2.28761 11.1211Z" fill="currentColor"/>
<path d="M6.42041 1.5C7.01664 1.5 7.49997 1.98337 7.49997 2.57959V8.81835C6.96373 8.4594 6.31878 8.25 5.62501 8.25C4.91854 8.25 4.26271 8.46705 3.7207 8.83815L5.01124 2.6455C5.15039 1.97822 5.73876 1.50007 6.42041 1.5Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.625 9C7.07475 9 8.25 10.1753 8.25 11.625C8.25 13.0747 7.07475 14.25 5.625 14.25C4.17525 14.25 3 13.0747 3 11.625C3 10.1753 4.17525 9 5.625 9ZM5.625 10.875C5.21078 10.875 4.875 11.2108 4.875 11.625C4.875 12.0392 5.21078 12.375 5.625 12.375C6.03921 12.375 6.375 12.0392 6.375 11.625C6.375 11.2108 6.03921 10.875 5.625 10.875Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,9 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.875 11.8125C7.875 13.1587 6.7837 14.25 5.4375 14.25C4.0913 14.25 3 13.1587 3 11.8125C3 10.4663 4.0913 9.375 5.4375 9.375C6.7837 9.375 7.875 10.4663 7.875 11.8125Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.25 15.75L5.625 14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.5 11.25L3 10.875" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.41699 12.5625C5.83121 12.5625 6.16699 12.2267 6.16699 11.8125C6.16699 11.3983 5.83121 11.0625 5.41699 11.0625C5.00278 11.0625 4.66699 11.3983 4.66699 11.8125C4.66699 12.2267 5.00278 12.5625 5.41699 12.5625Z" fill="currentColor"/>
<path d="M13.125 11.25L12.4956 8.10292C12.4255 7.75237 12.1177 7.5 11.7601 7.5H11.625C10.7966 7.5 10.125 8.17155 10.125 9V12.3939C10.125 13.419 10.956 14.25 11.9811 14.25C12.2408 14.25 12.4976 14.3045 12.7349 14.41L15.75 15.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.875 7.5V3.75C10.875 2.92157 11.5466 2.25 12.375 2.25H12.5394C12.8836 2.25 13.1836 2.48422 13.267 2.81811L15.3332 11.0833L16.5 11.625" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.125 9.75V3C7.125 2.58579 6.78921 2.25 6.375 2.25H5.52089C5.14964 2.25 4.83426 2.5216 4.77919 2.88875L3.75 9.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,6 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="agent" transform="translate(2.5 1)">
<path d="M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z" fill="currentColor"/>
<path d="M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z" fill="currentColor"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="agent">
<g id="Vector">
<path d="M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z" fill="white"/>
<path d="M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z" fill="white"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,10 +1,7 @@
{
"prefix": "custom-public",
"lastModified": 1781515983,
"lastModified": 1781246368,
"icons": {
"agent-building-blocks": {
"body": "<path fill=\"#155AEF\" fill-rule=\"evenodd\" d=\"M8.303 1.546c.178-.045.364-.051.544-.017c.23.043.432.167.573.246l3.757 2.113c.12.067.29.156.433.289l.06.06c.12.131.21.288.267.457c.07.215.063.445.063.6V9.56c0 .146.007.36-.056.563q-.055.181-.162.338l-.075.1c-.137.163-.32.274-.442.353l-5.013 3.259c-.135.088-.33.224-.556.282a1.3 1.3 0 0 1-.543.017c-.23-.043-.433-.166-.573-.245l-3.757-2.114c-.136-.077-.34-.182-.493-.35a1.3 1.3 0 0 1-.267-.456C1.993 11.09 2 10.86 2 10.704V6.441c0-.146-.007-.36.055-.563l.043-.118a1.3 1.3 0 0 1 .195-.32l.053-.059c.128-.131.282-.225.389-.294L7.86 1.755c.122-.078.273-.165.443-.209m-4.97 9.158l.001.164l.033.02l.11.062l3.264 1.836v-1.137L3.333 9.732zm4.741.917v1.076l4.464-2.901l.098-.065l.029-.02v-.034l.001-.118v-.923zm-4.74-3.419L6.74 10.12V8.982L3.333 7.066zm4.74.752v1.076l4.592-2.985V5.969zm.51-6.08l-4.631 3.01l3.429 1.93l4.664-3.032l-3.28-1.846l-.15-.082z\" clip-rule=\"evenodd\"/>"
},
"avatar-user": {
"body": "<g fill=\"none\"><g clip-path=\"url(#svgID0)\"><rect width=\"512\" height=\"512\" fill=\"#B2DDFF\" rx=\"256\"/><circle cx=\"256\" cy=\"196\" r=\"84\" fill=\"#fff\" opacity=\".68\"/><ellipse cx=\"256\" cy=\"583.5\" fill=\"#fff\" opacity=\".68\" rx=\"266\" ry=\"274.5\"/></g><defs><clipPath id=\"svgID0\"><rect width=\"512\" height=\"512\" fill=\"#fff\" rx=\"256\"/></clipPath></defs></g>",
"width": 512,

View File

@ -1,7 +1,7 @@
{
"prefix": "custom-public",
"name": "Dify Custom Public",
"total": 145,
"total": 144,
"version": "0.0.0-private",
"author": {
"name": "LangGenius, Inc.",
@ -13,12 +13,12 @@
"url": "https://github.com/langgenius/dify/blob/main/LICENSE"
},
"samples": [
"agent-building-blocks",
"avatar-user",
"billing-ar-cube-1",
"billing-asterisk",
"billing-aws-marketplace-dark",
"billing-aws-marketplace-light"
"billing-aws-marketplace-light",
"billing-azure"
],
"palette": false
}

View File

@ -1,29 +1,7 @@
{
"prefix": "custom-vender",
"lastModified": 1781515983,
"lastModified": 1781246368,
"icons": {
"agent-v2-access-point": {
"body": "<g fill=\"none\"><path d=\"M7.5 11.25C7.91421 11.25 8.25 11.5858 8.25 12V14.25C8.25 14.6642 7.91421 15 7.5 15C7.08579 15 6.75 14.6642 6.75 14.25V12C6.75 11.5858 7.08579 11.25 7.5 11.25Z\" fill=\"currentColor\"/><path d=\"M2.19653 2.19653C2.48937 1.90372 2.96418 1.90382 3.25708 2.19653L8.03027 6.96973C8.09162 7.03108 8.13966 7.10082 8.17529 7.1748C8.19164 7.20869 8.20587 7.24378 8.21704 7.28027C8.24638 7.37633 8.25641 7.477 8.24634 7.57617C8.23743 7.66451 8.21216 7.74788 8.17529 7.82446C8.13963 7.89868 8.09176 7.96874 8.03027 8.03027L3.25708 12.8035C2.96419 13.096 2.48932 13.0962 2.19653 12.8035C1.90394 12.5107 1.90405 12.0358 2.19653 11.7429L5.68945 8.25H0.75C0.335786 8.25 0 7.91421 0 7.5C0 7.08579 0.335786 6.75 0.75 6.75H5.68945L2.19653 3.25708C1.90389 2.96423 1.90388 2.48937 2.19653 2.19653Z\" fill=\"currentColor\"/><path d=\"M10.1521 10.1521C10.445 9.85921 10.9198 9.85921 11.2126 10.1521L12.8035 11.7429C13.096 12.0358 13.0962 12.5107 12.8035 12.8035C12.5107 13.0962 12.0358 13.096 11.7429 12.8035L10.1521 11.2126C9.85921 10.9198 9.85922 10.445 10.1521 10.1521Z\" fill=\"currentColor\"/><path d=\"M14.25 6.75C14.6642 6.75 15 7.08579 15 7.5C15 7.91421 14.6642 8.25 14.25 8.25H12C11.5858 8.25 11.25 7.91421 11.25 7.5C11.25 7.08579 11.5858 6.75 12 6.75H14.25Z\" fill=\"currentColor\"/><path d=\"M11.7422 2.19653C12.035 1.90387 12.5098 1.90406 12.8027 2.19653C13.0956 2.4894 13.0955 2.96419 12.8027 3.25708L11.2119 4.8479C10.919 5.14079 10.4443 5.1408 10.1514 4.8479C9.85883 4.55497 9.85858 4.08013 10.1514 3.78735L11.7422 2.19653Z\" fill=\"currentColor\"/><path d=\"M7.5 0C7.91421 0 8.25 0.335786 8.25 0.75V3C8.25 3.41421 7.91421 3.75 7.5 3.75C7.08579 3.75 6.75 3.41421 6.75 3V0.75C6.75 0.335786 7.08579 0 7.5 0Z\" fill=\"currentColor\"/></g>",
"width": 15,
"height": 15
},
"agent-v2-end-user-auth": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12 7.33325C13.1046 7.33325 14 8.22865 14 9.33325C14 10.1403 13.5218 10.8356 12.8333 11.1516V11.9999L12.3333 12.4999L12.8333 12.9511V13.6666L12 14.3333L11.1667 13.6666V11.1516C10.4782 10.8356 10 10.1403 10 9.33325C10 8.22865 10.8954 7.33325 12 7.33325ZM12 8.66659C11.6318 8.66659 11.3333 8.96505 11.3333 9.33325C11.3333 9.70145 11.6318 9.99992 12 9.99992C12.3682 9.99992 12.6667 9.70145 12.6667 9.33325C12.6667 8.96505 12.3682 8.66659 12 8.66659Z\" fill=\"currentColor\"/><path d=\"M8 7.99992C8.2545 7.99992 8.50382 8.01506 8.7474 8.04484L8.58594 9.36841C8.39687 9.34527 8.20127 9.33325 8 9.33325C5.8465 9.33325 4.25915 10.7274 3.78646 12.6666H10V13.9999H2.26758L2.33594 13.2708C2.61081 10.3473 4.82817 7.99992 8 7.99992Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8 1.33325C9.65687 1.33325 11 2.6764 11 4.33325C11 5.99011 9.65687 7.33325 8 7.33325C6.34315 7.33325 5 5.99011 5 4.33325C5 2.6764 6.34315 1.33325 8 1.33325ZM8 2.66659C7.07953 2.66659 6.33333 3.41278 6.33333 4.33325C6.33333 5.25373 7.07953 5.99992 8 5.99992C8.92047 5.99992 9.66667 5.25373 9.66667 4.33325C9.66667 3.41278 8.92047 2.66659 8 2.66659Z\" fill=\"currentColor\"/></g>"
},
"agent-v2-plan": {
"body": "<g fill=\"none\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M17 0C17.5523 0 18 0.447715 18 1V6C18 6.55228 17.5523 7 17 7H12C11.4477 7 11 6.55228 11 6V4.5H6.94629C5.92438 4.50039 5.56101 5.85276 6.44531 6.36523L12.5576 9.90332C15.2116 11.4402 14.1206 15.4996 11.0537 15.5H7V17C7 17.5523 6.55228 18 6 18H1C0.447715 18 0 17.5523 0 17V12C0 11.4477 0.447715 11 1 11H6C6.55228 11 7 11.4477 7 12V13.5H11.0537C12.0756 13.4996 12.4394 12.1472 11.5557 11.6348L5.44336 8.09668C2.789 6.55983 3.87917 2.50039 6.94629 2.5H11V1C11 0.447715 11.4477 0 12 0H17ZM2 16H5V13H2V16ZM13 5H16V2H13V5Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"agent-v2-prompt-insert": {
"body": "<g fill=\"none\"><path d=\"M2.91669 1.16669C1.95019 1.16669 1.16669 1.95019 1.16669 2.91669V11.0834C1.16669 12.0499 1.95019 12.8334 2.91669 12.8334H11.0834C12.0499 12.8334 12.8334 12.0499 12.8334 11.0834V2.91669C12.8334 1.95019 12.0499 1.16669 11.0834 1.16669H2.91669ZM2.33335 2.91669C2.33335 2.59452 2.59452 2.33335 2.91669 2.33335H11.0834C11.4055 2.33335 11.6667 2.59452 11.6667 2.91669V11.0834C11.6667 11.4055 11.4055 11.6667 11.0834 11.6667H2.91669C2.59452 11.6667 2.33335 11.4055 2.33335 11.0834V2.91669ZM5.67188 10.5L9.67186 3.50002H8.32815L4.32817 10.5H5.67188Z\" fill=\"currentColor\"/></g>",
"width": 14,
"height": 14
},
"agent-v2-robot-3": {
"body": "<g fill=\"none\"><path d=\"M6.25 6.875C6.82523 6.875 7.29167 7.34128 7.29167 7.91667V9.16667C7.29167 9.74205 6.82523 10.2083 6.25 10.2083C5.67477 10.2083 5.20833 9.74205 5.20833 9.16667V7.91667C5.20833 7.34128 5.67477 6.875 6.25 6.875Z\" fill=\"currentColor\"/><path d=\"M10.4167 6.875C10.992 6.875 11.4583 7.34135 11.4583 7.91667V9.16667C11.4583 9.74199 10.992 10.2083 10.4167 10.2083C9.84135 10.2083 9.375 9.74199 9.375 9.16667V7.91667C9.375 7.34135 9.84135 6.875 10.4167 6.875Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M8.33333 0C9.13875 0 9.79167 0.652918 9.79167 1.45833C9.79167 2.02329 9.46964 2.51173 8.99984 2.75391V3.33822C9.38912 3.34279 9.77995 3.35006 10.175 3.36263C11.6983 3.41112 12.7377 3.42425 13.6401 3.90951C14.375 4.30477 15.0255 4.97655 15.3971 5.72347C15.5468 6.02442 15.6427 6.33532 15.7056 6.66667H15.8333C16.2936 6.66667 16.6667 7.03976 16.6667 7.5V10C16.6667 10.4602 16.2936 10.8333 15.8333 10.8333H15.8285C15.8235 11.2254 15.813 11.5735 15.7869 11.8831C15.7386 12.4571 15.6361 12.9628 15.3971 13.4432C15.0254 14.1901 14.3749 14.8619 13.6401 15.2572C12.7377 15.7424 11.6982 15.7556 10.175 15.804C8.93336 15.8436 7.73328 15.8436 6.4917 15.804C4.96843 15.7556 3.92896 15.7424 3.02653 15.2572C2.29178 14.8619 1.64121 14.1902 1.26953 13.4432C1.03058 12.9628 0.928072 12.4571 0.87972 11.8831C0.853642 11.5735 0.843216 11.2254 0.838216 10.8333H0.833333C0.373096 10.8333 0 10.4602 0 10V7.5C0 7.03976 0.373096 6.66667 0.833333 6.66667H0.9611C1.02392 6.33532 1.11984 6.02442 1.26953 5.72347C1.64119 4.97649 2.29177 4.30475 3.02653 3.90951C3.92895 3.42425 4.96837 3.41112 6.4917 3.36263C6.88671 3.35006 7.27754 3.34279 7.66683 3.33822V2.75391C7.19703 2.51173 6.875 2.02329 6.875 1.45833C6.875 0.652918 7.52792 0 8.33333 0ZM10.1213 5.02848C8.91522 4.9901 7.75142 4.9901 6.54541 5.02848C4.85908 5.08217 4.29323 5.12091 3.81592 5.3776C3.38476 5.60954 2.98015 6.02734 2.76204 6.46566C2.65217 6.68652 2.57959 6.96168 2.54069 7.4235C2.50069 7.89854 2.5 8.50363 2.5 9.37825V9.78841C2.5 10.663 2.50069 11.2681 2.54069 11.7432C2.57959 12.205 2.65215 12.4801 2.76204 12.701C2.98015 13.1393 3.38475 13.5571 3.81592 13.7891C4.29321 14.0458 4.85904 14.0845 6.54541 14.1382C7.75141 14.1766 8.91523 14.1766 10.1213 14.1382C11.8075 14.0845 12.3734 14.0458 12.8507 13.7891C13.2819 13.5572 13.6865 13.1394 13.9046 12.701C14.0145 12.4801 14.0871 12.205 14.126 11.7432C14.166 11.2681 14.1667 10.663 14.1667 9.78841V9.37825C14.1667 8.50363 14.166 7.89854 14.126 7.4235C14.0871 6.96168 14.0145 6.68652 13.9046 6.46566C13.6865 6.02729 13.2819 5.60951 12.8507 5.3776C12.3734 5.12091 11.8075 5.08217 10.1213 5.02848Z\" fill=\"currentColor\"/></g>",
"width": 17
},
"features-citations": {
"body": "<g fill=\"none\"><path d=\"M1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM7 11.9702V14.958H11.0356V11.2339H8.8125C8.78418 10.8185 8.85498 10.4173 9.0249 10.0303C9.35531 9.29395 10.002 8.77474 10.9648 8.47266V7C9.67155 7.25488 8.68506 7.79297 8.00537 8.61426C7.33512 9.43555 7 10.5542 7 11.9702ZM15.0391 10.0586C15.3695 9.29395 16.0114 8.7653 16.9648 8.47266V7C15.7093 7.25488 14.7323 7.78825 14.0337 8.6001C13.3446 9.41195 13 10.5353 13 11.9702V14.958H17.0356V11.2339H14.8125C14.7747 10.8563 14.8503 10.4645 15.0391 10.0586Z\" fill=\"currentColor\"/></g>",
"width": 24,
@ -833,16 +811,6 @@
"width": 24,
"height": 24
},
"main-nav-roster": {
"body": "<g fill=\"none\"><path d=\"M7.875 11.8125C7.875 13.1587 6.7837 14.25 5.4375 14.25C4.0913 14.25 3 13.1587 3 11.8125C3 10.4663 4.0913 9.375 5.4375 9.375C6.7837 9.375 7.875 10.4663 7.875 11.8125Z\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M2.25 15.75L5.625 14.25\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M1.5 11.25L3 10.875\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M5.41699 12.5625C5.83121 12.5625 6.16699 12.2267 6.16699 11.8125C6.16699 11.3983 5.83121 11.0625 5.41699 11.0625C5.00278 11.0625 4.66699 11.3983 4.66699 11.8125C4.66699 12.2267 5.00278 12.5625 5.41699 12.5625Z\" fill=\"currentColor\"/><path d=\"M13.125 11.25L12.4956 8.10292C12.4255 7.75237 12.1177 7.5 11.7601 7.5H11.625C10.7966 7.5 10.125 8.17155 10.125 9V12.3939C10.125 13.419 10.956 14.25 11.9811 14.25C12.2408 14.25 12.4976 14.3045 12.7349 14.41L15.75 15.75\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M10.875 7.5V3.75C10.875 2.92157 11.5466 2.25 12.375 2.25H12.5394C12.8836 2.25 13.1836 2.48422 13.267 2.81811L15.3332 11.0833L16.5 11.625\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M7.125 9.75V3C7.125 2.58579 6.78921 2.25 6.375 2.25H5.52089C5.14964 2.25 4.83426 2.5216 4.77919 2.88875L3.75 9.75\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
"width": 18,
"height": 18
},
"main-nav-roster-active": {
"body": "<g fill=\"none\"><path d=\"M11.9362 1.5C12.4177 1.50006 12.8411 1.81975 12.9727 2.28296L15.4651 11.061C15.5117 11.2251 15.6125 11.3687 15.7515 11.4675L16.1235 11.7319C16.3544 11.8963 16.4728 12.1769 16.4297 12.4571L15.9206 15.7669C15.8394 16.2948 15.2475 16.5708 14.7905 16.2942L12.8006 15.0893C12.6837 15.0186 12.5618 14.9546 12.4307 14.9165C12.2411 14.8615 12.0443 14.833 11.8462 14.833C10.6885 14.833 9.75 13.8945 9.75 12.7368V9.14722C9.75 8.23747 10.4875 7.5 11.3972 7.5C11.5688 7.5 11.7164 7.62113 11.7503 7.78928L12.3824 10.9483L12.4043 11.0215C12.4721 11.182 12.6457 11.2781 12.8233 11.2427C13.0009 11.2072 13.1242 11.0515 13.125 10.8772L13.1177 10.8017L12.4856 7.6428C12.3818 7.12384 11.9265 6.75002 11.3972 6.75C11.08 6.75 10.7771 6.81165 10.5 6.92359V2.93628C10.5 2.14312 11.1431 1.5 11.9362 1.5Z\" fill=\"currentColor\"/><path d=\"M2.28761 11.1211C2.263 11.2855 2.25026 11.4538 2.25026 11.625C2.25026 13.3862 3.59948 14.8313 5.32057 14.9854L3.0757 16.3674C2.65801 16.6245 2.10961 16.418 1.96534 15.9492L0.926773 12.5742C0.823558 12.2388 0.967018 11.876 1.27174 11.7019L2.28761 11.1211Z\" fill=\"currentColor\"/><path d=\"M6.42041 1.5C7.01664 1.5 7.49997 1.98337 7.49997 2.57959V8.81835C6.96373 8.4594 6.31878 8.25 5.62501 8.25C4.91854 8.25 4.26271 8.46705 3.7207 8.83815L5.01124 2.6455C5.15039 1.97822 5.73876 1.50007 6.42041 1.5Z\" fill=\"currentColor\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M5.625 9C7.07475 9 8.25 10.1753 8.25 11.625C8.25 13.0747 7.07475 14.25 5.625 14.25C4.17525 14.25 3 13.0747 3 11.625C3 10.1753 4.17525 9 5.625 9ZM5.625 10.875C5.21078 10.875 4.875 11.2108 4.875 11.625C4.875 12.0392 5.21078 12.375 5.625 12.375C6.03921 12.375 6.375 12.0392 6.375 11.625C6.375 11.2108 6.03921 10.875 5.625 10.875Z\" fill=\"currentColor\"/></g>",
"width": 18,
"height": 18
},
"main-nav-studio": {
"body": "<g fill=\"none\"><path d=\"M15.8206 2.0275C15.7973 1.82217 15.6238 1.66696 15.4171 1.66675C15.2104 1.66654 15.0365 1.82139 15.0128 2.02667C14.865 3.30836 14.1416 4.03176 12.8599 4.17959C12.6547 4.20326 12.4998 4.37719 12.5 4.58383C12.5003 4.79047 12.6554 4.96408 12.8608 4.98733C14.1243 5.13046 14.8978 5.84689 15.0117 7.12955C15.0304 7.33946 15.2064 7.50032 15.4171 7.50008C15.6278 7.49984 15.8035 7.33859 15.8217 7.12863C15.9311 5.86411 16.6973 5.09787 17.9619 4.98841C18.1718 4.97023 18.3331 4.79461 18.3333 4.58387C18.3336 4.37313 18.1728 4.19715 17.9628 4.17851C16.6802 4.06457 15.9637 3.29101 15.8206 2.0275Z\" fill=\"currentColor\"/><path d=\"M7.29167 9.16659C8.9025 9.16659 10.2083 7.86075 10.2083 6.24992C10.2083 4.63909 8.9025 3.33325 7.29167 3.33325C5.68084 3.33325 4.375 4.63909 4.375 6.24992C4.375 7.86075 5.68084 9.16659 7.29167 9.16659Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M1.66699 16.6667C1.66699 13.9053 3.90557 11.6667 6.66699 11.6667H7.08366\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M9.16634 16.6666L10.833 10.8333H18.333L16.6663 16.6666H9.16634ZM9.16634 16.6666H5.83301\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></g>",
"width": 20,
@ -1327,9 +1295,9 @@
"height": 24
},
"workflow-agent": {
"body": "<g fill=\"none\"><g id=\"agent\" transform=\"translate(2.5 1)\"><path d=\"M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z\" fill=\"currentColor\"/><path d=\"M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z\" fill=\"currentColor\"/></g></g>",
"width": 24,
"height": 24
"body": "<g fill=\"none\"><g id=\"agent\"><g id=\"Vector\"><path d=\"M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z\" fill=\"currentColor\"/><path d=\"M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z\" fill=\"currentColor\"/></g></g></g>",
"width": 16,
"height": 16
},
"workflow-answer": {
"body": "<g fill=\"none\"><g id=\"icons/answer\"><path id=\"Vector (Stroke)\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M3.50114 1.67701L10.5011 1.677C11.5079 1.677 12.3241 2.49311 12.3241 3.49992V9.35414C12.3241 10.3609 11.5079 11.177 10.5012 11.1771H8.9954L7.41734 12.4845C7.17339 12.6866 6.81987 12.6856 6.57708 12.4821L5.02026 11.1771H3.50114C2.49436 11.1771 1.67822 10.3608 1.67822 9.35414V3.49993C1.67822 2.49316 2.49437 1.67701 3.50114 1.67701ZM10.5011 2.9895L3.50114 2.98951C3.21924 2.98951 2.99072 3.21803 2.99072 3.49993V9.35414C2.99072 9.63601 3.21926 9.86455 3.50114 9.86455H5.04675C5.33794 9.86455 5.61984 9.96705 5.84302 10.1541L7.00112 11.1249L8.17831 10.1496C8.40069 9.96537 8.68041 9.86455 8.96916 9.86455H10.5011C10.5011 9.86455 10.5011 9.86455 10.5011 9.86455C10.783 9.8645 11.0116 9.63592 11.0116 9.35414V3.49992C11.0116 3.21806 10.7831 2.9895 10.5011 2.9895ZM9.06809 4.93171C9.32437 5.18799 9.32437 5.60351 9.06809 5.85979L7.02642 7.90146C6.77014 8.15774 6.35464 8.15774 6.09835 7.90146L5.22333 7.02646C4.96704 6.77019 4.96704 6.35467 5.22332 6.09839C5.4796 5.8421 5.89511 5.8421 6.15139 6.09837L6.56238 6.50935L8.14001 4.93171C8.3963 4.67543 8.81181 4.67543 9.06809 4.93171Z\" fill=\"currentColor\"/></g></g>",

View File

@ -1,4 +1,4 @@
export interface IconifyJSON {
export type IconifyJSON = {
prefix: string
icons: Record<string, IconifyIcon>
aliases?: Record<string, IconifyAlias>
@ -7,7 +7,7 @@ export interface IconifyJSON {
lastModified?: number
}
export interface IconifyIcon {
export type IconifyIcon = {
body: string
left?: number
top?: number
@ -18,11 +18,11 @@ export interface IconifyIcon {
vFlip?: boolean
}
export interface IconifyAlias extends Omit<IconifyIcon, 'body'> {
export type IconifyAlias = {
parent: string
}
} & Omit<IconifyIcon, 'body'>
export interface IconifyInfo {
export type IconifyInfo = {
prefix: string
name: string
total: number
@ -40,11 +40,11 @@ export interface IconifyInfo {
palette?: boolean
}
export interface IconifyMetaData {
export type IconifyMetaData = {
[key: string]: unknown
}
export interface IconifyChars {
export type IconifyChars = {
[key: string]: string
}

View File

@ -1,8 +1,8 @@
'use strict'
const chars = require('./chars.json')
const icons = require('./icons.json')
const info = require('./info.json')
const metadata = require('./metadata.json')
const chars = require('./chars.json')
module.exports = { icons, info, metadata, chars }

View File

@ -1,6 +1,6 @@
import chars from './chars.json' with { type: 'json' }
import icons from './icons.json' with { type: 'json' }
import info from './info.json' with { type: 'json' }
import metadata from './metadata.json' with { type: 'json' }
import chars from './chars.json' with { type: 'json' }
export { icons, info, metadata, chars }
export { chars, icons, info, metadata }

View File

@ -1,7 +1,7 @@
{
"prefix": "custom-vender",
"name": "Dify Custom Vender",
"total": 326,
"total": 319,
"version": "0.0.0-private",
"author": {
"name": "LangGenius, Inc.",
@ -13,12 +13,12 @@
"url": "https://github.com/langgenius/dify/blob/main/LICENSE"
},
"samples": [
"agent-v2-access-point",
"agent-v2-end-user-auth",
"agent-v2-plan",
"agent-v2-prompt-insert",
"agent-v2-robot-3",
"features-citations"
"features-citations",
"features-content-moderation",
"features-document",
"features-folder-upload",
"features-love-message",
"features-message-fast"
],
"palette": false
}

View File

@ -6,7 +6,7 @@ import Loading from '@/app/components/base/loading'
import { redirect, usePathname } from '@/next/navigation'
import { consoleQuery } from '@/service/client'
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/snippets', '/roster', '/explore', '/tools', '/integrations'] as const
const datasetOperatorRedirectRoutes = ['/', '/apps', '/app', '/snippets', '/explore', '/tools', '/integrations'] as const
const isPathUnderRoute = (pathname: string, route: string) => pathname === route || pathname.startsWith(`${route}/`)

View File

@ -1,13 +0,0 @@
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
return <AgentDetailPage agentId={agentId} section="access" />
}

View File

@ -1,13 +0,0 @@
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
return <AgentDetailPage agentId={agentId} section="configure" />
}

View File

@ -1,20 +0,0 @@
import type { ReactNode } from 'react'
import { AgentDetailLayout } from '@/features/agent-v2/agent-detail/layout'
type LayoutProps = {
children: ReactNode
params: Promise<{ agentId: string }>
}
export default async function Layout({
children,
params,
}: LayoutProps) {
const { agentId } = await params
return (
<AgentDetailLayout agentId={agentId}>
{children}
</AgentDetailLayout>
)
}

View File

@ -1,13 +0,0 @@
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
return <AgentDetailPage agentId={agentId} section="logs" />
}

View File

@ -1,13 +0,0 @@
import { AgentDetailPage } from '@/features/agent-v2/agent-detail/page'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
return <AgentDetailPage agentId={agentId} section="monitoring" />
}

View File

@ -1,13 +0,0 @@
import { redirect } from '@/next/navigation'
type PageProps = {
params: Promise<{ agentId: string }>
}
export default async function Page({
params,
}: PageProps) {
const { agentId } = await params
redirect(`/roster/agent/${agentId}/configure`)
}

View File

@ -1,5 +0,0 @@
import RosterPage from '@/features/agent-v2/roster/page'
export default function Page() {
return <RosterPage />
}

View File

@ -19,7 +19,6 @@ import { useInfiniteDatasets } from '@/service/knowledge/use-dataset'
type ISelectDataSetProps = {
isShow: boolean
modal?: boolean
onClose: () => void
selectedIds: string[]
onSelect: (dataSet: DataSet[]) => void
@ -27,7 +26,6 @@ type ISelectDataSetProps = {
const SelectDataSet: FC<ISelectDataSetProps> = ({
isShow,
modal,
onClose,
selectedIds,
onSelect,
@ -92,8 +90,8 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
}, [handleClose])
return (
<Dialog modal={modal} open={isShow} onOpenChange={handleOpenChange}>
<DialogContent backdropProps={{ forceRender: true }} className="w-100 overflow-hidden">
<Dialog open={isShow} onOpenChange={handleOpenChange}>
<DialogContent className="w-100 overflow-hidden">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t('feature.dataSet.selectTitle', { ns: 'appDebug' })}
</DialogTitle>

View File

@ -30,7 +30,6 @@ import { RetrievalChangeTip, RetrievalSection } from './retrieval-section'
type SettingsModalProps = {
currentDataset: DataSet
height?: string
onCancel: () => void
onSave: (newDataset: DataSet) => void
}
@ -45,7 +44,6 @@ const labelClass = `
const SettingsModal: FC<SettingsModalProps> = ({
currentDataset,
height = 'calc(100vh - 72px)',
onCancel,
onSave,
}) => {
@ -188,9 +186,9 @@ const SettingsModal: FC<SettingsModalProps> = ({
return (
<div
className="flex min-h-0 w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
className="flex w-full flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
style={{
height,
height: 'calc(100vh - 72px)',
}}
ref={ref}
>

View File

@ -8,18 +8,10 @@ import { useHover } from 'ahooks'
import { cva } from 'class-variance-authority'
import { init } from 'emoji-mart'
import * as React from 'react'
import { useRef, useSyncExternalStore } from 'react'
import { useRef } from 'react'
init({ data })
const subscribeHydrationState = () => () => {}
const useIsHydrated = () => useSyncExternalStore(
subscribeHydrationState,
() => true,
() => false,
)
type AppIconProps = {
size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | 'xl' | 'xxl'
rounded?: boolean
@ -113,20 +105,9 @@ const AppIcon: FC<AppIconProps> = ({
}) => {
const isValidImageIcon = iconType === 'image' && imageUrl
const emojiIcon = (icon && icon !== '') ? icon : '🤖'
const isHydrated = useIsHydrated()
const Icon = isHydrated ? <em-emoji key={emojiIcon} id={emojiIcon} /> : emojiIcon
const Icon = <em-emoji key={emojiIcon} id={emojiIcon} />
const wrapperRef = useRef<HTMLSpanElement>(null)
const isHovering = useHover(wrapperRef)
const handleKeyDown = (event: React.KeyboardEvent<HTMLSpanElement>) => {
if (!onClick)
return
if (event.key !== 'Enter' && event.key !== ' ')
return
event.preventDefault()
onClick()
}
return (
<span
@ -134,9 +115,6 @@ const AppIcon: FC<AppIconProps> = ({
className={cn(appIconVariants({ size, rounded }), className)}
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
onClick={onClick}
onKeyDown={onClick ? handleKeyDown : undefined}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
>
{
isValidImageIcon

View File

@ -1,4 +1,3 @@
import type { ReactNode } from 'react'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import type { InputVar } from '@/app/components/workflow/types'
import type { PromptVariable } from '@/models/debug'
@ -27,13 +26,9 @@ type Props = Readonly<{
onClose: () => void
inWorkflow?: boolean
showFileUpload?: boolean
showModeration?: boolean
promptVariables?: PromptVariable[]
workflowVariables?: InputVar[]
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
title?: ReactNode
description?: ReactNode
drawerClassName?: string
}>
const NewFeaturePanel = ({
@ -44,13 +39,9 @@ const NewFeaturePanel = ({
onClose,
inWorkflow = true,
showFileUpload = true,
showModeration = true,
promptVariables,
workflowVariables,
onAutoAddPromptVariable,
title,
description,
drawerClassName,
}: Props) => {
const { t } = useTranslation()
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
@ -61,14 +52,13 @@ const NewFeaturePanel = ({
show={show}
onClose={onClose}
inWorkflow={inWorkflow}
className={drawerClassName}
>
<div className="flex h-full grow flex-col">
{/* header */}
<div className="flex shrink-0 justify-between p-4 pb-3">
<div>
<div className="system-xl-semibold text-text-primary">{title ?? t('common.features', { ns: 'workflow' })}</div>
<div className="body-xs-regular text-text-tertiary">{description ?? t('common.featuresDescription', { ns: 'workflow' })}</div>
<div className="system-xl-semibold text-text-primary">{t('common.features', { ns: 'workflow' })}</div>
<div className="body-xs-regular text-text-tertiary">{t('common.featuresDescription', { ns: 'workflow' })}</div>
</div>
<DrawerCloseButton
aria-label={t('operation.close', { ns: 'common' })}
@ -103,7 +93,7 @@ const NewFeaturePanel = ({
{isChatMode && (
<Citation disabled={disabled} onChange={onChange} />
)}
{showModeration && (isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
{(isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
{!inWorkflow && isChatMode && (
<AnnotationReply disabled={disabled} onChange={onChange} />
)}

View File

@ -302,37 +302,6 @@ describe('ModerationSettingModal', () => {
}))
})
it('should save the latest preset response when content textarea changes', async () => {
const data: ModerationConfig = {
...defaultData,
config: {
keywords: 'bad',
inputs_config: { enabled: true, preset_response: 'blocked' },
outputs_config: { enabled: false, preset_response: '' },
},
}
await renderModal(
<ModerationSettingModal
data={data}
onCancel={vi.fn()}
onSave={onSave}
/>,
)
fireEvent.change(screen.getByRole('textbox', { name: /feature\.moderation\.modal\.content\.preset/ }), {
target: { value: 'updated blocked response' },
})
fireEvent.click(screen.getByText(/operation\.save/))
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({
config: expect.objectContaining({
inputs_config: expect.objectContaining({
preset_response: 'updated blocked response',
}),
}),
}))
})
it('should show api selector when api type is selected', async () => {
await renderModal(
<ModerationSettingModal
@ -733,10 +702,7 @@ describe('ModerationSettingModal', () => {
expect(mockSetShowAccountSettingModal).toHaveBeenCalled()
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
payload: 'provider',
onCancelCallback: expect.any(Function),
})
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
})
it('should not save when OpenAI type is selected but not configured', async () => {

View File

@ -2,7 +2,6 @@ import type { FC } from 'react'
import type { ModerationContentConfig } from '@/models/debug'
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
type ModerationContentProps = {
@ -20,71 +19,57 @@ const ModerationContent: FC<ModerationContentProps> = ({
onConfigChange,
}) => {
const { t } = useTranslation()
const [presetResponse, setPresetResponse] = useState(config.preset_response || '')
const handleConfigChange = (field: string, value: boolean | string) => {
if (field === 'preset_response' && typeof value === 'string')
value = value.slice(0, 100)
onConfigChange({
...config,
preset_response: field === 'preset_response' ? value as string : presetResponse,
[field]: value,
})
}
const handlePresetResponseChange = (value: string) => {
const nextValue = value.slice(0, 100)
setPresetResponse(nextValue)
handleConfigChange('preset_response', nextValue)
onConfigChange({ ...config, [field]: value })
}
return (
<div className="rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs">
<div className="flex min-h-10 items-center gap-2 px-3 py-2">
<div className="min-w-0 flex-1 system-sm-medium text-text-secondary">{title}</div>
<div className="flex min-w-0 shrink-0 items-center justify-end">
{
info && (
<div className="mr-2 truncate system-xs-regular text-text-tertiary" title={info}>{info}</div>
)
}
<Switch
checked={config.enabled}
onCheckedChange={v => handleConfigChange('enabled', v)}
/>
<div className="py-2">
<div className="rounded-lg border border-components-panel-border bg-components-panel-bg">
<div className="flex h-10 items-center justify-between rounded-lg px-3">
<div className="shrink-0 text-sm font-medium text-text-primary">{title}</div>
<div className="flex grow items-center justify-end">
{
info && (
<div className="mr-2 truncate text-xs text-text-tertiary" title={info}>{info}</div>
)
}
<Switch
size="lg"
checked={config.enabled}
onCheckedChange={v => handleConfigChange('enabled', v)}
/>
</div>
</div>
</div>
{
config.enabled && showPreset && (
<div className="px-3 pt-0.5 pb-3">
<div className="flex h-8 items-center justify-between gap-2">
<span className="system-2xs-medium-uppercase text-text-secondary">
{
config.enabled && showPreset && (
<div className="rounded-lg bg-components-panel-bg px-3 pt-1 pb-3">
<div className="flex h-8 items-center justify-between text-[13px] font-medium text-text-secondary">
{t('feature.moderation.modal.content.preset', { ns: 'appDebug' })}
</span>
<span className="flex shrink-0 items-center gap-0.5 rounded bg-background-section px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
<span className="i-ri-markdown-line size-3" aria-hidden />
{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}
</span>
</div>
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
<div className="relative h-20">
<Textarea
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
value={presetResponse}
className="size-full resize-none pb-8"
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
onValueChange={handlePresetResponseChange}
/>
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 system-2xs-medium-uppercase text-text-quaternary">
<span>{presetResponse.length}</span>
/
<span className="text-text-tertiary">100</span>
<span className="text-xs font-normal text-text-tertiary">{t('feature.moderation.modal.content.supportMarkdown', { ns: 'appDebug' })}</span>
</div>
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
<div className="relative h-20">
<Textarea
aria-label={t('feature.moderation.modal.content.preset', { ns: 'appDebug' }) as string}
value={config.preset_response || ''}
className="size-full resize-none pb-8"
placeholder={t('feature.moderation.modal.content.placeholder', { ns: 'appDebug' }) || ''}
onValueChange={value => handleConfigChange('preset_response', value)}
/>
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
<span>{(config.preset_response || '').length}</span>
/
<span className="text-text-tertiary">100</span>
</div>
</div>
</div>
</div>
)
}
)
}
</div>
</div>
)
}

View File

@ -1,4 +1,4 @@
import type { FC, ReactNode } from 'react'
import type { FC } from 'react'
import type { CodeBasedExtensionItem } from '@/models/common'
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
import { Button } from '@langgenius/dify-ui/button'
@ -6,7 +6,7 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { useCallback, useRef, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { ApiBasedExtensionSelector } from '@/app/components/header/account-setting/api-based-extension-page/selector'
@ -27,27 +27,6 @@ type Provider = {
form_schema?: CodeBasedExtensionItem['form_schema']
}
function ProviderIcon({ type }: { type: string }) {
if (type === 'openai_moderation')
return <span className="i-ri-openai-fill size-4 text-text-secondary" aria-hidden />
if (type === 'keywords')
return <span className="i-ri-search-line size-4 text-util-colors-green-green-600" aria-hidden />
return <span className="i-ri-image-line size-4 text-util-colors-violet-violet-600" aria-hidden />
}
function LabeledDivider({ children }: { children: ReactNode }) {
return (
<div className="flex w-full items-center gap-2">
<span className="shrink-0 system-xs-medium-uppercase text-text-tertiary">
{children}
</span>
<Divider bgStyle="gradient" className="my-0 h-px flex-1" />
</div>
)
}
type ModerationSettingModalProps = {
data: ModerationConfig
onCancel: () => void
@ -62,27 +41,12 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
const { t } = useTranslation()
const docLink = useDocLink()
const locale = useLocale()
const { data: modelProviders, isPending: isLoading, refetch: refetchModelProviders } = useModelProviders()
const localeDataRef = useRef<ModerationConfig>(data)
const { data: modelProviders, isPending: isLoading } = useModelProviders()
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
const openIntegrationsSetting = useIntegrationsSetting()
const updateLocaleData = useCallback((
update: ModerationConfig | ((current: ModerationConfig) => ModerationConfig),
options: { render?: boolean } = {},
) => {
const nextLocaleData = typeof update === 'function'
? update(localeDataRef.current)
: update
localeDataRef.current = nextLocaleData
if (options.render !== false)
setLocaleData(nextLocaleData)
}, [])
const handleOpenSettingsModal = () => {
openIntegrationsSetting({
payload: ACCOUNT_SETTING_TAB.PROVIDER,
onCancelCallback: refetchModelProviders,
})
}
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
@ -121,20 +85,20 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
const currentProvider = providers.find(provider => provider.key === localeData.type)
const handleDataTypeChange = (type: string) => {
let config: undefined | Record<string, string>
let config: undefined | Record<string, any>
const currProvider = providers.find(provider => provider.key === type)
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
config = currProvider?.form_schema.reduce((prev, next) => {
prev[next.variable] = next.default
return prev
}, {} as Record<string, string>)
}, {} as Record<string, any>)
}
updateLocaleData(current => ({
...current,
setLocaleData({
...localeData,
type,
config,
}))
})
}
const handleDataKeywordsChange = (value: string) => {
@ -147,46 +111,43 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
return prev
}, [])
updateLocaleData(current => ({
...current,
setLocaleData({
...localeData,
config: {
...current.config,
...localeData.config,
keywords: arr.slice(0, 100).join('\n'),
},
}))
})
}
const handleDataContentChange = (contentType: string, contentConfig: ModerationContentConfig) => {
const previousContentConfig = localeDataRef.current.config?.[contentType] as ModerationContentConfig | undefined
const shouldRender = previousContentConfig?.enabled !== contentConfig.enabled
updateLocaleData(current => ({
...current,
setLocaleData({
...localeData,
config: {
...current.config,
...localeData.config,
[contentType]: contentConfig,
},
}), { render: shouldRender })
})
}
const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
updateLocaleData(current => ({
...current,
setLocaleData({
...localeData,
config: {
...current.config,
...localeData.config,
api_based_extension_id: apiBasedExtensionId,
},
}))
})
}
const handleDataExtraChange = (extraValue: Record<string, string>) => {
updateLocaleData(current => ({
...current,
setLocaleData({
...localeData,
config: {
...current.config,
...localeData.config,
...extraValue,
},
}))
})
}
const formatData = (originData: ModerationConfig) => {
@ -218,116 +179,115 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
}
const handleSave = () => {
const currentLocaleData = localeDataRef.current
const providerForSave = providers.find(provider => provider.key === currentLocaleData.type)
/* v8 ignore next -- UI-invariant guard: same condition is used in Save button disabled logic, so when true handleSave has no user-triggerable invocation path. @preserve */
if (currentLocaleData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
if (localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured)
return
if (!currentLocaleData.config?.inputs_config?.enabled && !currentLocaleData.config?.outputs_config?.enabled) {
if (!localeData.config?.inputs_config?.enabled && !localeData.config?.outputs_config?.enabled) {
toast.error(t('feature.moderation.modal.content.condition', { ns: 'appDebug' }))
return
}
if (currentLocaleData.type === 'keywords' && !currentLocaleData.config.keywords) {
if (localeData.type === 'keywords' && !localeData.config.keywords) {
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }))
return
}
if (currentLocaleData.type === 'api' && !currentLocaleData.config.api_based_extension_id) {
if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }))
return
}
if (systemTypes.findIndex(t => t === currentLocaleData.type) < 0 && providerForSave?.form_schema) {
for (let i = 0; i < providerForSave.form_schema.length; i++) {
if (!currentLocaleData.config?.[providerForSave.form_schema[i]!.variable] && providerForSave.form_schema[i]!.required) {
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? providerForSave.form_schema[i]!.label['en-US'] : providerForSave.form_schema[i]!.label['zh-Hans'] }))
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
for (let i = 0; i < currentProvider.form_schema.length; i++) {
if (!localeData.config?.[currentProvider.form_schema[i]!.variable] && currentProvider.form_schema[i]!.required) {
toast.error(t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i]!.label['en-US'] : currentProvider.form_schema[i]!.label['zh-Hans'] }))
return
}
}
}
if (currentLocaleData.config.inputs_config?.enabled && !currentLocaleData.config.inputs_config.preset_response && currentLocaleData.type !== 'api') {
if (localeData.config.inputs_config?.enabled && !localeData.config.inputs_config.preset_response && localeData.type !== 'api') {
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
return
}
if (currentLocaleData.config.outputs_config?.enabled && !currentLocaleData.config.outputs_config.preset_response && currentLocaleData.type !== 'api') {
if (localeData.config.outputs_config?.enabled && !localeData.config.outputs_config.preset_response && localeData.type !== 'api') {
toast.error(t('feature.moderation.modal.content.errorMessage', { ns: 'appDebug' }))
return
}
onSave(formatData(currentLocaleData))
onSave(formatData(localeData))
}
return (
<Dialog open>
<DialogContent className="mt-14! w-[600px]! max-w-none! overflow-hidden border-[0.5px]! border-components-panel-border! p-0! text-left align-middle">
<div className="flex items-start gap-2 px-6 pt-6 pr-14 pb-3">
<div className="title-2xl-semi-bold text-text-primary">
{t('feature.moderation.modal.title', { ns: 'appDebug' })}
</div>
<DialogContent className="mt-14! w-[600px]! max-w-none! border-none p-6! text-left align-middle">
<div className="flex items-center justify-between">
<div className="title-2xl-semi-bold text-text-primary">{t('feature.moderation.modal.title', { ns: 'appDebug' })}</div>
<button
type="button"
aria-label={t('operation.close', { ns: 'common' })}
className="absolute top-5 right-5 flex size-8 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
className="cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
onClick={onCancel}
>
<span className="i-ri-close-line size-[18px]" aria-hidden="true" />
<span className="i-ri-close-line size-4 text-text-tertiary" aria-hidden="true" />
</button>
</div>
<div className="flex flex-col gap-4 px-6 py-3">
<div className="flex flex-col gap-1">
<div className="system-sm-medium text-text-secondary">
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
</div>
<div className="grid grid-cols-3 gap-2">
{providers.map(provider => (
<button
type="button"
<div className="py-2">
<div className="text-sm/9 font-medium text-text-primary">
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
</div>
<div className="grid grid-cols-3 gap-2.5">
{
providers.map(provider => (
<div
key={provider.key}
className={cn(
'flex min-h-[68px] flex-col items-start justify-center gap-1.5 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2 text-left text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
localeData.type !== provider.key && 'hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
'flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 system-sm-regular text-text-secondary',
localeData.type !== provider.key && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg system-sm-medium shadow-xs',
localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled',
)}
onClick={() => handleDataTypeChange(provider.key)}
>
<div className="flex size-8 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-background-default-dodge">
<ProviderIcon type={provider.key} />
<div className={cn(
'mr-2 size-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
localeData.type === provider.key && 'border-[5px] border-components-radio-border-checked',
)}
>
</div>
<span className="w-full truncate system-xs-regular">
{provider.name}
</span>
</button>
))}
</div>
{!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
{provider.name}
</div>
))
}
</div>
{
!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
<div className="mt-2 flex items-center rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] px-3 py-2">
<span className="mr-1 i-custom-vender-line-general-info-circle h-4 w-4 text-[#F79009]" />
<div className="flex items-center text-xs font-medium text-gray-700">
{t('feature.moderation.modal.openaiNotConfig.before', { ns: 'appDebug' })}
<button
type="button"
<span
className="cursor-pointer text-primary-600"
onClick={handleOpenSettingsModal}
>
&nbsp;
&nbsp;
{t('settings.provider', { ns: 'common' })}
&nbsp;
</button>
&nbsp;
</span>
{t('feature.moderation.modal.openaiNotConfig.after', { ns: 'appDebug' })}
</div>
</div>
)}
</div>
{localeData.type === 'keywords' && (
<div className="flex flex-col gap-1">
<div className="system-sm-medium text-text-secondary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
<div className="system-xs-regular text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
)
}
</div>
{
localeData.type === 'keywords' && (
<div className="py-2">
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
{/* Keep this counter composed locally; extract only if more textarea counter cases repeat. */}
<div className="relative h-[88px]">
<Textarea
@ -337,7 +297,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
className="size-full resize-none pb-8"
placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''}
/>
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 system-2xs-medium-uppercase text-text-quaternary">
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
/
<span className="text-text-tertiary">
@ -347,16 +307,18 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
</div>
</div>
</div>
)}
{localeData.type === 'api' && (
<div className="flex flex-col gap-1">
<div className="flex h-6 items-center justify-between">
<div className="system-sm-medium text-text-secondary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
)
}
{
localeData.type === 'api' && (
<div className="py-2">
<div className="flex h-9 items-center justify-between">
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
<a
href={docLink('/use-dify/workspace/api-extension/api-extension')}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center system-xs-regular text-text-tertiary hover:text-primary-600"
className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"
>
<span className="mr-1 i-custom-vender-line-education-book-open-01 size-3 text-text-tertiary group-hover:text-primary-600" />
{t('apiBasedExtension.link', { ns: 'common' })}
@ -367,51 +329,46 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
onChange={handleDataApiBasedChange}
/>
</div>
)}
{systemTypes.findIndex(t => t === localeData.type) < 0
&& currentProvider?.form_schema
&& (
<FormGeneration
forms={currentProvider?.form_schema}
value={localeData.config}
onChange={handleDataExtraChange}
/>
)}
<div className="flex flex-col gap-2">
<LabeledDivider>{t('feature.moderation.title', { ns: 'appDebug' })}</LabeledDivider>
<ModerationContent
key={`inputs-${localeData.type}-${localeData.config?.inputs_config?.preset_response ?? ''}`}
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('inputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
)
}
{
systemTypes.findIndex(t => t === localeData.type) < 0
&& currentProvider?.form_schema
&& (
<FormGeneration
forms={currentProvider?.form_schema}
value={localeData.config}
onChange={handleDataExtraChange}
/>
<ModerationContent
key={`outputs-${localeData.type}-${localeData.config?.outputs_config?.preset_response ?? ''}`}
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('outputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<div className="py-0.5 system-xs-regular text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
</div>
</div>
<div className="flex h-[76px] items-center justify-end gap-2 px-6 pt-5 pb-6">
)
}
<Divider bgStyle="gradient" className="my-3 h-px" />
<ModerationContent
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('inputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<ModerationContent
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('outputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<div className="mt-1 mb-8 text-xs font-medium text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
<div className="flex items-center justify-end">
<Button
onClick={onCancel}
size="medium"
className="min-w-[72px]"
className="mr-2"
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
variant="primary"
size="medium"
onClick={handleSave}
disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
className="min-w-[72px]"
>
{t('operation.save', { ns: 'common' })}
</Button>

View File

@ -4,9 +4,9 @@
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
@ -15,27 +15,35 @@
"type": "element",
"name": "g",
"attributes": {
"id": "agent",
"transform": "translate(2.5 1)"
"id": "agent"
},
"children": [
{
"type": "element",
"name": "path",
"name": "g",
"attributes": {
"d": "M3.3178 20.9524V15.7184C3.31774 15.7136 3.31456 15.6845 3.28404 15.6468L3.24312 15.6069C1.26589 13.9971 0 11.5398 0 8.78813C0.000122972 3.93464 3.93464 0.000122969 8.78813 0C13.5146 0 17.3698 3.73066 17.5691 8.40858C17.5712 8.45846 17.5841 8.48161 17.5865 8.48531L19.3226 11.089C19.7937 11.7956 19.6309 12.7481 18.9513 13.2579L17.5998 14.2707C17.5851 14.2819 17.5763 14.2996 17.5763 14.3178V15.4237C17.576 17.2235 16.1176 18.682 14.3178 18.6822H13.2119C13.1798 18.6822 13.1537 18.7085 13.1536 18.7405V20.9524C13.1536 21.5309 12.6845 21.9999 12.1059 22C11.5274 22 11.0583 21.531 11.0583 20.9524V18.7405C11.0584 17.5513 12.0226 16.587 13.2119 16.587H14.3178C14.9604 16.5868 15.4808 16.0663 15.481 15.4237V14.3178C15.481 13.64 15.8006 13.0016 16.3424 12.595L17.3195 11.8614L15.8432 9.64853C15.603 9.28835 15.4913 8.88291 15.4749 8.49758C15.323 4.93639 12.3872 2.09524 8.78813 2.09524C5.09181 2.09536 2.09536 5.09181 2.09524 8.78813C2.09524 10.883 3.05647 12.7533 4.56594 13.9822C5.0571 14.3822 5.41299 15.0012 5.41304 15.7184V20.9524C5.41304 21.5309 4.94385 21.9998 4.36542 22C3.78684 22 3.3178 21.531 3.3178 20.9524Z",
"fill": "currentColor"
"id": "Vector"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.79012 6.5163L9.31429 5.27923C9.23058 5.06159 9.02158 4.91799 8.78836 4.91799C8.55514 4.91799 8.34614 5.06159 8.26243 5.27923L7.7866 6.5163C7.56194 7.10037 7.10038 7.56194 6.5163 7.78659L5.27923 8.26239C5.06159 8.34609 4.91799 8.55519 4.91799 8.78836C4.91799 9.02158 5.06159 9.23058 5.27923 9.31429L6.5163 9.79012C7.10037 10.0148 7.56194 10.4764 7.7866 11.0604L8.26243 12.2975C8.34614 12.5151 8.55514 12.6587 8.78836 12.6587C9.02158 12.6587 9.23058 12.5151 9.31429 12.2975L9.79012 11.0604C10.0148 10.4764 10.4764 10.0148 11.0604 9.79012L12.2975 9.31429C12.5151 9.23058 12.6587 9.02158 12.6587 8.78836C12.6587 8.55519 12.5151 8.34609 12.2975 8.26239L11.0604 7.78659C10.4764 7.56194 10.0148 7.10038 9.79012 6.5163Z",
"fill": "currentColor"
},
"children": []
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M14.7401 5.80454C14.5765 4.77996 14.1638 3.79808 13.5306 2.97273C12.8973 2.14738 12.0648 1.48568 11.1185 1.06589C10.1722 0.646098 9.12632 0.461106 8.08751 0.546487C7.05582 0.624753 6.04548 0.966277 5.17744 1.53548C4.3094 2.09758 3.58366 2.88024 3.09272 3.79808C2.59466 4.70881 2.33852 5.7405 2.33852 6.7793V7.22756L1.25703 9.3692C1.04357 9.80322 1.22145 10.3368 1.65547 10.5574L2.3314 10.8989V12.3006C2.3314 12.82 2.53063 13.3038 2.90061 13.6738C3.2706 14.0367 3.75442 14.243 4.27382 14.243H6.01702V14.7624C6.01702 15.1538 6.3372 15.4739 6.72853 15.4739C7.11986 15.4739 7.44004 15.1538 7.44004 14.7624V13.7094C7.44004 13.2185 7.04159 12.82 6.55065 12.82H4.27382C4.13864 12.82 4.00345 12.7631 3.91095 12.6706C3.81846 12.5781 3.76154 12.4429 3.76154 12.3077V10.5716C3.76154 10.2301 3.56943 9.92417 3.2706 9.77476L2.77254 9.52573L3.66904 7.73984C3.72596 7.61889 3.76154 7.4837 3.76154 7.34851V6.77219C3.76154 5.96818 3.96076 5.17129 4.34498 4.4669C4.72919 3.76251 5.28417 3.15772 5.9601 2.7237C6.63603 2.28968 7.41158 2.02643 8.20847 1.96239C9.00536 1.89835 9.81648 2.04066 10.5493 2.36795C11.2822 2.69524 11.9225 3.20042 12.4135 3.84077C12.8973 4.47402 13.2246 5.23533 13.3456 6.02511C13.4665 6.81488 13.3954 7.63312 13.125 8.38731C12.8617 9.12017 12.4206 9.78187 11.8585 10.3084C11.6735 10.4792 11.5668 10.7139 11.5668 10.9701V14.7624C11.5668 15.1538 11.887 15.4739 12.2783 15.4739C12.6696 15.4739 12.9898 15.1538 12.9898 14.7624V11.1978C13.6515 10.5432 14.1567 9.73918 14.4697 8.87114C14.8184 7.89637 14.918 6.83623 14.7615 5.81165L14.7401 5.80454Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M10.8055 7.99599C10.8909 7.83234 10.962 7.66158 11.0189 7.4837H11.6522C12.0435 7.4837 12.3637 7.16352 12.3637 6.77219C12.3637 6.38086 12.0435 6.06068 11.6522 6.06068H11.0189C10.9691 5.8828 10.898 5.71204 10.8055 5.54839L11.2537 5.10014C11.5312 4.82266 11.5312 4.3744 11.2537 4.09692C10.9762 3.81943 10.528 3.81943 10.2505 4.09692L9.80225 4.54517C9.6386 4.45267 9.46784 4.38863 9.28996 4.33171V3.69847C9.28996 3.30714 8.96978 2.98696 8.57845 2.98696C8.18712 2.98696 7.86694 3.30714 7.86694 3.69847V4.33171C7.68907 4.38152 7.5183 4.45267 7.35466 4.54517L6.90641 4.09692C6.62892 3.81943 6.18067 3.81943 5.90318 4.09692C5.62569 4.3744 5.62569 4.82266 5.90318 5.10014L6.35143 5.54839C6.26605 5.71204 6.1949 5.8828 6.13798 6.06068H5.50473C5.1134 6.06068 4.79323 6.38086 4.79323 6.77219C4.79323 7.16352 5.1134 7.4837 5.50473 7.4837H6.13798C6.18778 7.66158 6.25893 7.83234 6.35143 7.99599L5.90318 8.44424C5.62569 8.72172 5.62569 9.16997 5.90318 9.44746C6.04548 9.58976 6.22336 9.6538 6.40835 9.6538C6.59334 9.6538 6.77122 9.58265 6.91352 9.44746L7.36177 8.99921C7.52542 9.08459 7.69618 9.15574 7.87406 9.21267V9.84591C7.87406 10.2372 8.19424 10.5574 8.58557 10.5574C8.9769 10.5574 9.29708 10.2372 9.29708 9.84591V9.21267C9.47496 9.16286 9.64572 9.09171 9.80936 8.99921L10.2576 9.44746C10.3999 9.58976 10.5778 9.6538 10.7628 9.6538C10.9478 9.6538 11.1257 9.58265 11.268 9.44746C11.5454 9.16997 11.5454 8.72172 11.268 8.44424L10.8197 7.99599H10.8055ZM7.44004 6.77219C7.44004 6.14606 7.94521 5.64089 8.57134 5.64089C9.19747 5.64089 9.70264 6.14606 9.70264 6.77219C9.70264 7.39832 9.19747 7.90349 8.57134 7.90349C7.94521 7.90349 7.44004 7.39832 7.44004 6.77219Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}

View File

@ -9,13 +9,6 @@ import {
UPDATE_HISTORY_EVENT_EMITTER,
} from '../constants'
import PromptEditor from '../index'
import { CustomTextNode } from '../plugins/custom-text/node'
type MockNodeReplacementConfig = {
replace?: unknown
with?: (arg: { __text: string }) => void
withKlass?: unknown
}
const mocks = vi.hoisted(() => {
const commandHandlers = new Map<unknown, (payload: unknown) => boolean>()
@ -25,7 +18,6 @@ const mocks = vi.hoisted(() => {
return {
emit: vi.fn(),
rootLines: ['first line', 'second line'],
nodeReplacementConfig: undefined as MockNodeReplacementConfig | undefined,
commandHandlers,
subscriptions,
rootElement,
@ -94,7 +86,7 @@ vi.mock('@lexical/react/LexicalComposer', () => ({
LexicalComposer: ({ initialConfig, children }: {
initialConfig: {
onError?: (error: Error) => void
nodes?: unknown[]
nodes?: Array<{ replace?: unknown, with: (arg: { __text: string }) => void }>
}
children: ReactNode
}) => {
@ -107,11 +99,9 @@ vi.mock('@lexical/react/LexicalComposer', () => ({
}
}
if (initialConfig?.nodes) {
const textNodeConf = initialConfig.nodes.find((node): node is MockNodeReplacementConfig => {
return typeof node === 'object' && node !== null && 'replace' in node
})
mocks.nodeReplacementConfig = textNodeConf
textNodeConf?.with?.({ __text: 'test' })
const textNodeConf = initialConfig.nodes.find((n: { replace?: unknown, with: (arg: { __text: string }) => void }) => n?.replace)
if (textNodeConf)
textNodeConf.with({ __text: 'test' })
}
return <div data-testid="lexical-composer">{children}</div>
},
@ -183,17 +173,10 @@ describe('PromptEditor', () => {
mocks.commandHandlers.clear()
mocks.subscriptions.length = 0
mocks.rootLines = ['first line', 'second line']
mocks.nodeReplacementConfig = undefined
})
// Rendering shell and text output from lexical state.
describe('Rendering', () => {
it('should register CustomTextNode as the TextNode replacement class', () => {
render(<PromptEditor />)
expect(mocks.nodeReplacementConfig?.withKlass).toBe(CustomTextNode)
})
it('should render placeholder and call onChange with joined lexical text', async () => {
const onChange = vi.fn()

View File

@ -26,7 +26,6 @@ import { HITLInputNode } from '../plugins/hitl-input-block'
import { LastRunBlockNode } from '../plugins/last-run-block'
import { QueryBlockNode } from '../plugins/query-block'
import { RequestURLBlockNode } from '../plugins/request-url-block'
import { RosterReferenceBlockNode } from '../plugins/roster-reference-block/node'
import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '../plugins/update-block'
import { VariableValueBlockNode } from '../plugins/variable-value-block/node'
import { WorkflowVariableBlockNode } from '../plugins/workflow-variable-block'
@ -109,7 +108,6 @@ const PromptEditorContentHarness = ({
RequestURLBlockNode,
WorkflowVariableBlockNode,
VariableValueBlockNode,
RosterReferenceBlockNode,
HITLInputNode,
CurrentBlockNode,
ErrorMessageBlockNode,
@ -293,29 +291,5 @@ describe('PromptEditorContent', () => {
expect(screen.queryByTestId('draggable-target-line')).not.toBeInTheDocument()
expect(screen.getByText('common.promptEditor.placeholder')).toBeInTheDocument()
})
it('should render roster references as inline token pills when enabled', async () => {
const captures: Captures = { editor: null, eventEmitter: null }
const { container } = render(
<PromptEditorContentHarness
captures={captures}
shortcutPopups={[]}
initialText="Use [§file:file-1:qna_report.pdf§]"
floatingAnchorElem={null}
onEditorChange={vi.fn()}
rosterReferenceBlock={{ show: true }}
/>,
)
await waitFor(() => {
expect(captures.editor).not.toBeNull()
})
const token = container.querySelector('[data-roster-reference-kind="file"]') as HTMLElement
expect(token).toBeInTheDocument()
expect(token).toHaveTextContent('qna_report.pdf')
expect(token.querySelector('.i-ri-file-pdf-2-fill')).toBeInTheDocument()
})
})
})

View File

@ -16,7 +16,6 @@ import type {
LastRunBlockType,
QueryBlockType,
RequestURLBlockType,
RosterReferenceBlockType,
VariableBlockType,
WorkflowVariableBlockType,
} from './types'
@ -61,7 +60,6 @@ import {
import {
RequestURLBlockNode,
} from './plugins/request-url-block'
import { RosterReferenceBlockNode } from './plugins/roster-reference-block/node'
import { VariableValueBlockNode } from './plugins/variable-value-block/node'
import {
WorkflowVariableBlockNode,
@ -110,7 +108,6 @@ const EditableSyncPlugin: FC<{ editable: boolean }> = ({ editable }) => {
export type PromptEditorProps = {
instanceId?: string
children?: React.ReactNode
compact?: boolean
wrapperClassName?: string
className?: string
@ -127,7 +124,6 @@ export type PromptEditorProps = {
requestURLBlock?: RequestURLBlockType
historyBlock?: HistoryBlockType
variableBlock?: VariableBlockType
rosterReferenceBlock?: RosterReferenceBlockType
externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType
hitlInputBlock?: HITLInputBlockType
@ -135,8 +131,6 @@ export type PromptEditorProps = {
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean
disableSlashPicker?: boolean
disableBracePicker?: boolean
shortcutPopups?: Array<{
hotkey: Hotkey
displayMode?: ShortcutPopupDisplayMode
@ -146,7 +140,6 @@ export type PromptEditorProps = {
const PromptEditor: FC<PromptEditorProps> = ({
instanceId,
children,
compact,
wrapperClassName,
className,
@ -163,7 +156,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
requestURLBlock,
historyBlock,
variableBlock,
rosterReferenceBlock,
externalToolBlock,
workflowVariableBlock,
hitlInputBlock,
@ -171,8 +163,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
errorMessageBlock,
lastRunBlock,
isSupportFileVar,
disableSlashPicker = false,
disableBracePicker = false,
shortcutPopups = [],
}) => {
const { eventEmitter } = useEventEmitterContextContext()
@ -187,7 +177,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
{
replace: TextNode,
with: (node: TextNode) => new CustomTextNode(node.__text),
withKlass: CustomTextNode,
},
ContextBlockNode,
HistoryBlockNode,
@ -195,7 +184,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
RequestURLBlockNode,
WorkflowVariableBlockNode,
VariableValueBlockNode,
RosterReferenceBlockNode,
HITLInputNode,
CurrentBlockNode,
ErrorMessageBlockNode,
@ -254,7 +242,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
requestURLBlock={requestURLBlock}
historyBlock={historyBlock}
variableBlock={variableBlock}
rosterReferenceBlock={rosterReferenceBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
hitlInputBlock={hitlInputBlock}
@ -262,8 +249,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
disableSlashPicker={disableSlashPicker}
disableBracePicker={disableBracePicker}
onBlur={onBlur}
onFocus={onFocus}
instanceId={instanceId}
@ -272,7 +257,6 @@ const PromptEditor: FC<PromptEditorProps> = ({
/>
<ValueSyncPlugin value={value} />
<EditableSyncPlugin editable={editable} />
{children}
</div>
</LexicalComposer>
)

View File

@ -1,119 +0,0 @@
import type {
Klass,
LexicalEditor,
LexicalNode,
} from 'lexical'
import { render, screen } from '@testing-library/react'
import { createEditor } from 'lexical'
import RosterReferenceBlockComponent from '../component'
import {
$createRosterReferenceBlockNode,
$isRosterReferenceBlockNode,
RosterReferenceBlockNode,
} from '../node'
import {
getRosterReferenceFileIconType,
parseRosterReferenceToken,
} from '../utils'
describe('RosterReferenceBlockNode', () => {
let editor: LexicalEditor
beforeEach(() => {
vi.clearAllMocks()
editor = createEditor({
nodes: [RosterReferenceBlockNode as unknown as Klass<LexicalNode>],
})
})
const runInEditor = (callback: () => void) => {
editor.update(callback, { discrete: true })
}
it('should parse roster reference tokens and infer icon classes', () => {
expect(parseRosterReferenceToken('[§skill:2c3176de8a01:tender-analyzer§]')).toEqual({
kind: 'skill',
id: '2c3176de8a01',
label: 'tender-analyzer',
})
expect(parseRosterReferenceToken('[§file:1f0ad3e2:qna_report:final.pdf§]')).toEqual({
kind: 'file',
id: '1f0ad3e2',
label: 'qna_report:final.pdf',
})
expect(parseRosterReferenceToken('[§unknown:1:item§]')).toBeNull()
expect(getRosterReferenceFileIconType('qna_report.pdf')).toBe('pdf')
})
it('should render a non-editable token pill component', () => {
const { container } = render(
<RosterReferenceBlockComponent text="[§tool-all:tavily/tavily:tavily§]" />,
)
const token = screen.getByTitle('tavily')
expect(token).toHaveAttribute('contenteditable', 'false')
expect(token).toHaveAttribute('data-roster-reference-kind', 'tool-all')
expect(token).toHaveAttribute('data-roster-reference-id', 'tavily/tavily')
expect(token).toHaveClass('border-state-accent-hover-alt')
expect(token).toHaveClass('bg-state-accent-hover')
expect(token).toHaveTextContent('tavily')
expect(container.querySelector('.i-custom-public-other-default-tool-icon')).toBeInTheDocument()
})
it('should render knowledge icon with the configured retrieval row style', () => {
const { container } = render(
<RosterReferenceBlockComponent text="[§knowledge:manual-1:产品手册§]" />,
)
const iconShell = container.querySelector('.bg-util-colors-green-green-500')
expect(iconShell).toBeInTheDocument()
expect(iconShell).toHaveClass('text-text-primary-on-surface')
expect(iconShell?.querySelector('.i-ri-book-open-line')).toBeInTheDocument()
})
it('should expose DecoratorNode behavior and preserve raw text content', () => {
runInEditor(() => {
const node = new RosterReferenceBlockNode('[§tool-all:tavily/tavily:tavily§]', 'node-key')
const cloned = RosterReferenceBlockNode.clone(node)
const dom = node.createDOM()
expect(RosterReferenceBlockNode.getType()).toBe('roster-reference-block')
expect(cloned).toBeInstanceOf(RosterReferenceBlockNode)
expect(cloned.getKey()).toBe('node-key')
expect(node.isInline()).toBe(true)
expect(dom).toHaveClass('inline-flex')
expect(dom).toHaveClass('align-middle')
expect(node.getTextContent()).toBe('[§tool-all:tavily/tavily:tavily§]')
})
})
it('should import and export serialized node text', () => {
runInEditor(() => {
const imported = RosterReferenceBlockNode.importJSON({
text: '[§knowledge:manual-1:产品手册§]',
type: 'roster-reference-block',
version: 1,
})
const exported = imported.exportJSON()
expect(exported).toEqual({
text: '[§knowledge:manual-1:产品手册§]',
type: 'roster-reference-block',
version: 1,
})
})
})
it('should create node with helper and support type guard checks', () => {
runInEditor(() => {
const node = $createRosterReferenceBlockNode('[§skill:playwright:Playwright§]')
expect(node).toBeInstanceOf(RosterReferenceBlockNode)
expect(node.getTextContent()).toBe('[§skill:playwright:Playwright§]')
expect($isRosterReferenceBlockNode(node)).toBe(true)
expect($isRosterReferenceBlockNode(null)).toBe(false)
expect($isRosterReferenceBlockNode(undefined)).toBe(false)
expect($isRosterReferenceBlockNode({} as LexicalNode)).toBe(false)
})
})
})

View File

@ -1,55 +0,0 @@
import { cn } from '@langgenius/dify-ui/cn'
import { FileTreeIcon } from '@langgenius/dify-ui/file-tree'
import { use } from 'react'
import { RosterReferenceBlockContext } from './context'
import {
getRosterReferenceFileIconType,
getRosterReferenceIconClassName,
parseRosterReferenceToken,
} from './utils'
type RosterReferenceBlockComponentProps = {
text: string
}
const RosterReferenceBlockComponent = ({
text,
}: RosterReferenceBlockComponentProps) => {
const rosterReferenceBlock = use(RosterReferenceBlockContext)
const token = parseRosterReferenceToken(text)
if (!token)
return null
const isKnowledge = token.kind === 'knowledge'
const customIcon = rosterReferenceBlock?.renderIcon?.(token)
const defaultIcon = token.kind === 'file'
? <FileTreeIcon type={getRosterReferenceFileIconType(token.label)} className="size-4" />
: <span className={cn(isKnowledge ? 'size-3.5' : 'size-3.5 shrink-0', getRosterReferenceIconClassName(token))} />
return (
<span
contentEditable={false}
data-roster-reference-kind={token.kind}
data-roster-reference-id={token.id}
title={token.label}
className="inline-flex min-w-[18px] items-center gap-0.5 overflow-hidden rounded-[5px] border border-state-accent-hover-alt bg-state-accent-hover py-px pr-1 pl-px align-middle shadow-xs shadow-shadow-shadow-3"
>
<span
aria-hidden
className={cn(
'inline-flex size-4 shrink-0 items-center justify-center rounded-[5px] border-[0.5px] border-divider-subtle bg-background-default-dodge',
token.kind === 'cli_tool' && 'border-divider-regular bg-text-tertiary',
isKnowledge && 'border-divider-subtle bg-util-colors-green-green-500 p-[3px] text-text-primary-on-surface shadow-xs shadow-shadow-shadow-3',
)}
>
{customIcon || defaultIcon}
</span>
<span className="max-w-48 truncate system-xs-medium text-text-accent">
{token.label}
</span>
</span>
)
}
export default RosterReferenceBlockComponent

View File

@ -1,4 +0,0 @@
import type { RosterReferenceBlockType } from '../../types'
import { createContext } from 'react'
export const RosterReferenceBlockContext = createContext<RosterReferenceBlockType | undefined>(undefined)

View File

@ -1,63 +0,0 @@
import type { EntityMatch } from '@lexical/text'
import type { LexicalEditor, TextNode } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { mergeRegister } from '@lexical/utils'
import { $applyNodeReplacement } from 'lexical'
import {
useCallback,
useEffect,
} from 'react'
import { decoratorTransform } from '../../utils'
import { CustomTextNode } from '../custom-text/node'
import { RosterReferenceBlockNode } from './node'
import { ROSTER_REFERENCE_REGEX } from './utils'
type RosterReferenceNodeRegistry = {
_nodes: Map<string, { klass: typeof RosterReferenceBlockNode }>
}
function createRegisteredRosterReferenceBlockNode(editor: LexicalEditor, textNode: TextNode): RosterReferenceBlockNode {
const RegisteredRosterReferenceBlockNode = (editor as unknown as RosterReferenceNodeRegistry)
._nodes
.get(RosterReferenceBlockNode.getType())
?.klass ?? RosterReferenceBlockNode
return $applyNodeReplacement(new RegisteredRosterReferenceBlockNode(textNode.getTextContent()))
}
const RosterReferenceBlock = () => {
const [editor] = useLexicalComposerContext()
useEffect(() => {
if (!editor.hasNodes([RosterReferenceBlockNode]))
throw new Error('RosterReferenceBlockPlugin: RosterReferenceBlockNode not registered on editor')
}, [editor])
const createRosterReferenceBlockNode = useCallback((textNode: CustomTextNode): RosterReferenceBlockNode => (
createRegisteredRosterReferenceBlockNode(editor, textNode)
), [editor])
const getRosterReferenceMatch = useCallback((text: string): EntityMatch | null => {
const matchArr = ROSTER_REFERENCE_REGEX.exec(text)
if (matchArr === null)
return null
const startOffset = matchArr.index
const endOffset = startOffset + matchArr[0].length
return {
end: endOffset,
start: startOffset,
}
}, [])
useEffect(() => {
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getRosterReferenceMatch, createRosterReferenceBlockNode)),
)
}, [createRosterReferenceBlockNode, editor, getRosterReferenceMatch])
return null
}
export default RosterReferenceBlock

View File

@ -1,76 +0,0 @@
import type {
LexicalNode,
NodeKey,
SerializedLexicalNode,
} from 'lexical'
import type { JSX } from 'react'
import {
$applyNodeReplacement,
DecoratorNode,
} from 'lexical'
import RosterReferenceBlockComponent from './component'
type SerializedRosterReferenceBlockNode = SerializedLexicalNode & {
text: string
}
export class RosterReferenceBlockNode extends DecoratorNode<JSX.Element> {
__text: string
static override getType(): string {
return 'roster-reference-block'
}
static override clone(node: RosterReferenceBlockNode): RosterReferenceBlockNode {
return new RosterReferenceBlockNode(node.__text, node.__key)
}
constructor(text: string, key?: NodeKey) {
super(key)
this.__text = text
}
override isInline(): boolean {
return true
}
override createDOM(): HTMLElement {
const span = document.createElement('span')
span.classList.add('inline-flex', 'items-center', 'align-middle')
return span
}
override updateDOM(): false {
return false
}
override decorate(): JSX.Element {
return <RosterReferenceBlockComponent text={this.getTextContent()} />
}
static override importJSON(serializedNode: SerializedRosterReferenceBlockNode): RosterReferenceBlockNode {
return $createRosterReferenceBlockNode(serializedNode.text)
}
override exportJSON(): SerializedRosterReferenceBlockNode {
return {
text: this.getTextContent(),
type: 'roster-reference-block',
version: 1,
}
}
override getTextContent(): string {
return this.getLatest().__text
}
}
export function $createRosterReferenceBlockNode(text = ''): RosterReferenceBlockNode {
return $applyNodeReplacement(new RosterReferenceBlockNode(text))
}
export function $isRosterReferenceBlockNode(
node: LexicalNode | null | undefined,
): node is RosterReferenceBlockNode {
return node instanceof RosterReferenceBlockNode
}

View File

@ -1,111 +0,0 @@
import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree'
export type RosterReferenceKind = 'skill' | 'file' | 'tool-all' | 'tool' | 'cli_tool' | 'knowledge'
export type RosterReferenceToken = {
kind: RosterReferenceKind
id: string
label: string
}
export const ROSTER_REFERENCE_REGEX = /\[§(?:skill|file|tool-all|tool|cli_tool|knowledge):[^\]§\n\r]+§\]/
const KNOWN_KINDS = new Set<RosterReferenceKind>([
'skill',
'file',
'tool-all',
'tool',
'cli_tool',
'knowledge',
])
export function parseRosterReferenceToken(text: string): RosterReferenceToken | null {
if (!text.startsWith('[§') || !text.endsWith('§]'))
return null
const body = text.slice(2, -2)
const firstColonIndex = body.indexOf(':')
if (firstColonIndex === -1)
return null
const kind = body.slice(0, firstColonIndex) as RosterReferenceKind
if (!KNOWN_KINDS.has(kind))
return null
const rest = body.slice(firstColonIndex + 1)
const secondColonIndex = rest.indexOf(':')
const id = secondColonIndex === -1 ? rest : rest.slice(0, secondColonIndex)
const label = secondColonIndex === -1 ? id : rest.slice(secondColonIndex + 1)
if (!id || !label)
return null
return {
kind,
id,
label,
}
}
const codeFileExtensions = new Set([
'css',
'go',
'html',
'htm',
'js',
'jsx',
'py',
'rb',
'rs',
'scss',
'sh',
'ts',
'tsx',
'vue',
'yaml',
'yml',
])
const imageFileExtensions = new Set(['apng', 'avif', 'bmp', 'gif', 'ico', 'jpeg', 'jpg', 'png', 'svg', 'webp'])
const tableFileExtensions = new Set(['csv', 'xls', 'xlsx'])
const archiveFileExtensions = new Set(['7z', 'gz', 'rar', 'tar', 'zip'])
export function getRosterReferenceFileIconType(label: string): FileTreeIconType {
const extension = label.includes('.') ? label.split('.').pop()?.toLowerCase() : undefined
if (!extension)
return 'folder'
if (imageFileExtensions.has(extension))
return 'image'
if (extension === 'pdf')
return 'pdf'
if (extension === 'md' || extension === 'markdown' || extension === 'mdx')
return 'markdown'
if (extension === 'json')
return 'json'
if (tableFileExtensions.has(extension))
return 'table'
if (archiveFileExtensions.has(extension))
return 'archive'
if (codeFileExtensions.has(extension))
return 'code'
if (extension === 'txt')
return 'text'
return 'file'
}
export function getRosterReferenceIconClassName(token: RosterReferenceToken) {
switch (token.kind) {
case 'skill':
return 'i-custom-public-agent-building-blocks text-text-tertiary'
case 'file':
return ''
case 'tool-all':
case 'tool':
return 'i-custom-public-other-default-tool-icon text-[#ef5b39]'
case 'cli_tool':
return 'i-ri-terminal-box-line text-text-primary-on-surface'
case 'knowledge':
return 'i-ri-book-open-line'
}
}

View File

@ -11,7 +11,6 @@ import type {
LastRunBlockType,
QueryBlockType,
RequestURLBlockType,
RosterReferenceBlockType,
VariableBlockType,
WorkflowVariableBlockType,
} from './types'
@ -58,8 +57,6 @@ import {
RequestURLBlock,
RequestURLBlockReplacementBlock,
} from './plugins/request-url-block'
import RosterReferenceBlock from './plugins/roster-reference-block'
import { RosterReferenceBlockContext } from './plugins/roster-reference-block/context'
import ShortcutsPopupPlugin from './plugins/shortcuts-popup-plugin'
import UpdateBlock from './plugins/update-block'
import VariableBlock from './plugins/variable-block'
@ -87,7 +84,6 @@ type PromptEditorContentProps = {
requestURLBlock?: RequestURLBlockType
historyBlock?: HistoryBlockType
variableBlock?: VariableBlockType
rosterReferenceBlock?: RosterReferenceBlockType
externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType
hitlInputBlock?: HITLInputBlockType
@ -95,8 +91,6 @@ type PromptEditorContentProps = {
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean
disableSlashPicker?: boolean
disableBracePicker?: boolean
onBlur?: () => void
onFocus?: () => void
instanceId?: string
@ -116,7 +110,6 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
requestURLBlock,
historyBlock,
variableBlock,
rosterReferenceBlock,
externalToolBlock,
workflowVariableBlock,
hitlInputBlock,
@ -124,8 +117,6 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
errorMessageBlock,
lastRunBlock,
isSupportFileVar,
disableSlashPicker,
disableBracePicker,
onBlur,
onFocus,
instanceId,
@ -133,7 +124,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
onEditorChange,
}) => {
return (
<RosterReferenceBlockContext value={rosterReferenceBlock}>
<>
<RichTextPlugin
contentEditable={(
<ContentEditable
@ -159,38 +150,34 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
{(closePortal, onInsert) => <Popup onClose={closePortal} onInsert={onInsert} />}
</ShortcutsPopupPlugin>
))}
{!disableSlashPicker && (
<ComponentPickerBlock
triggerString="/"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
requestURLBlock={requestURLBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
)}
{!disableBracePicker && (
<ComponentPickerBlock
triggerString="{"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
requestURLBlock={requestURLBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
)}
<ComponentPickerBlock
triggerString="/"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
requestURLBlock={requestURLBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
<ComponentPickerBlock
triggerString="{"
contextBlock={contextBlock}
historyBlock={historyBlock}
queryBlock={queryBlock}
requestURLBlock={requestURLBlock}
variableBlock={variableBlock}
externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar}
/>
{contextBlock?.show && (
<>
<ContextBlock {...contextBlock} />
@ -215,9 +202,6 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
<VariableValueBlock />
</>
)}
{rosterReferenceBlock?.show && (
<RosterReferenceBlock />
)}
{workflowVariableBlock?.show && (
<>
<WorkflowVariableBlock {...workflowVariableBlock} />
@ -264,7 +248,7 @@ const PromptEditorContent: FC<PromptEditorContentProps> = ({
{floatingAnchorElem && (
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
)}
</RosterReferenceBlockContext>
</>
)
}

View File

@ -3,7 +3,6 @@ import type { FormInputItem } from '../../workflow/nodes/human-input/types'
import type { Type } from '../../workflow/nodes/llm/types'
import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block'
import type { RosterReferenceToken } from './plugins/roster-reference-block/utils'
import type {
Node,
NodeOutPutVar,
@ -60,11 +59,6 @@ export type VariableBlockType = {
variables?: Option[]
}
export type RosterReferenceBlockType = {
show?: boolean
renderIcon?: (token: RosterReferenceToken) => React.ReactNode
}
export type ExternalToolBlockType = {
show?: boolean
externalTools?: ExternalToolOption[]

View File

@ -40,25 +40,4 @@ describe('useIntegrationsSetting', () => {
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'mcp' })
})
it('should preserve the agent source for agent-scoped settings', () => {
const { result } = renderHook(() => useIntegrationsSetting())
act(() => {
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: 'agent' })
})
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider', source: 'agent' })
})
it('should preserve the cancel callback for migrated integrations settings', () => {
const onCancelCallback = vi.fn()
const { result } = renderHook(() => useIntegrationsSetting())
act(() => {
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, onCancelCallback })
})
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider', onCancelCallback })
})
})

View File

@ -4,7 +4,6 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { useCallback } from 'react'
type DialogProps = {
backdropClassName?: string
className?: string
children: ReactNode
show: boolean
@ -12,7 +11,6 @@ type DialogProps = {
}
const MenuDialog = ({
backdropClassName,
className,
children,
show,
@ -29,7 +27,7 @@ const MenuDialog = ({
}}
>
<DialogContent
backdropClassName={cn('z-40 bg-transparent', backdropClassName)}
backdropClassName="z-40 bg-transparent"
className={cn(
'top-0 left-0 z-40 h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 scale-100 overflow-hidden rounded-none border-none bg-background-sidenav-bg p-0 shadow-none backdrop-blur-md transition-opacity data-ending-style:scale-100 data-starting-style:scale-100',
className,

View File

@ -247,7 +247,6 @@ export type DefaultModelResponse = {
export type DefaultModel = {
provider: string
model: string
plugin_id?: string
}
export type CustomConfigurationModelFixedFields = {

View File

@ -108,11 +108,7 @@ describe('ModelSelector', () => {
fireEvent.click(screen.getByRole('combobox'))
fireEvent.click(screen.getByText('select'))
expect(onSelect).toHaveBeenCalledWith({
provider: 'openai',
model: 'gpt-4',
plugin_id: 'langgenius/openai',
})
expect(onSelect).toHaveBeenCalledWith({ provider: 'openai', model: 'gpt-4' })
})
it('should close popup when popup requests hide', () => {

View File

@ -11,15 +11,6 @@ import ModelSelectorTrigger from './model-selector-trigger'
import Popup from './popup'
import { getModelSelectorValueLabel, isSameModelSelectorValue } from './types'
const getModelProviderPluginId = (provider: string) => {
const [organization, pluginName] = provider.split('/').filter(Boolean)
if (organization && pluginName)
return `${organization}/${pluginName}`
return provider ? `langgenius/${provider}` : ''
}
type ModelSelectorProps = {
defaultModel?: DefaultModel
modelList: Model[]
@ -33,7 +24,6 @@ type ModelSelectorProps = {
showDeprecatedWarnIcon?: boolean
hideProviderSettingsFooter?: boolean
onConfigureEmptyState?: () => void
providerSettingsSource?: 'agent'
showModelMeta?: boolean
}
function ModelSelector({
@ -49,7 +39,6 @@ function ModelSelector({
showDeprecatedWarnIcon = true,
hideProviderSettingsFooter,
onConfigureEmptyState,
providerSettingsSource,
showModelMeta,
}: ModelSelectorProps) {
const { t } = useTranslation()
@ -85,13 +74,8 @@ function ModelSelector({
setOpen(false)
setInputValue('')
if (onSelect) {
onSelect({
provider,
model: model.model,
plugin_id: getModelProviderPluginId(provider),
})
}
if (onSelect)
onSelect({ provider, model: model.model })
}, [onSelect])
const handleValueChange = useCallback((value: ModelSelectorValue | null) => {
@ -166,7 +150,6 @@ function ModelSelector({
modelList={modelList}
scopeFeatures={scopeFeatures}
hideProviderSettingsFooter={hideProviderSettingsFooter}
providerSettingsSource={providerSettingsSource}
onConfigureEmptyState={onConfigureEmptyState ? handleConfigureEmptyState : undefined}
onInputValueChange={setInputValue}
onHide={handleHide}

View File

@ -38,7 +38,6 @@ export type PopupProps = {
modelList: Model[]
scopeFeatures?: ModelFeatureEnum[]
hideProviderSettingsFooter?: boolean
providerSettingsSource?: 'agent'
onConfigureEmptyState?: () => void
onInputValueChange: (value: string) => void
onHide: () => void
@ -49,7 +48,6 @@ function Popup({
modelList,
scopeFeatures = [],
hideProviderSettingsFooter,
providerSettingsSource,
onConfigureEmptyState,
onInputValueChange,
onHide,
@ -175,8 +173,8 @@ function Popup({
const handleOpenSettings = useCallback(() => {
onHide()
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: providerSettingsSource })
}, [onHide, openIntegrationsSetting, providerSettingsSource])
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
}, [onHide, openIntegrationsSetting])
const handleClosePreviewCard = useCallback(() => {
previewCardHandle.close()
}, [previewCardHandle])

View File

@ -7,8 +7,8 @@ import { useModalContext } from '@/context/modal-context'
import { integrationSectionByMovedAccountSettingTab } from './destinations'
type IntegrationsSettingState
= | { payload: MovedAccountSettingTab, source?: 'agent', onCancelCallback?: () => void }
| { section: IntegrationSection, source?: 'agent', onCancelCallback?: () => void }
= | { payload: MovedAccountSettingTab }
| { section: IntegrationSection }
export const useIntegrationsSetting = () => {
const { setShowAccountSettingModal } = useModalContext()
@ -19,12 +19,7 @@ export const useIntegrationsSetting = () => {
? state.section
: integrationSectionByMovedAccountSettingTab[state.payload]
if (section) {
setShowAccountSettingModal({
payload: section,
...(state.source ? { source: state.source } : {}),
...(state.onCancelCallback ? { onCancelCallback: state.onCancelCallback } : {}),
})
}
if (section)
setShowAccountSettingModal({ payload: section })
}, [setShowAccountSettingModal])
}

View File

@ -159,15 +159,6 @@ vi.mock('@/app/components/app-sidebar/dataset-detail-top', () => ({
),
}))
vi.mock('@/features/agent-v2/agent-detail/navigation', () => ({
AgentDetailSection: ({ expand }: { expand: boolean }) => <div data-testid="agent-detail-section" data-expand={expand} />,
AgentDetailTop: ({ expand, onToggle }: { expand: boolean, onToggle: () => void }) => (
<div data-testid="agent-detail-top" data-expand={expand}>
<button type="button" data-testid="agent-detail-toggle" onClick={onToggle}>Toggle</button>
</div>
),
}))
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
@ -329,7 +320,6 @@ describe('MainNav', () => {
expect(screen.getByRole('button', { name: 'common.account.account' })).not.toHaveTextContent(Plan.team)
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/')
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /common.menus.roster/ })).toHaveAttribute('href', '/roster')
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/integrations/model-provider')
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/marketplace')
@ -443,7 +433,6 @@ describe('MainNav', () => {
expect(screen.queryByRole('link', { name: /common.mainNav.home/ })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.apps/ })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
expect(screen.queryByRole('link', { name: /common.mainNav.integrations/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/marketplace')
@ -467,7 +456,6 @@ describe('MainNav', () => {
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.menus.roster/ })).toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.datasets/ })).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toBeInTheDocument()
@ -621,35 +609,6 @@ describe('MainNav', () => {
expect(screen.getByTestId('dataset-detail-section')).toHaveAttribute('data-expand', 'true')
})
it('replaces global navigation with agent detail navigation on roster detail routes', () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
expect(screen.getByTestId('agent-detail-top')).toBeInTheDocument()
expect(screen.getByTestId('agent-detail-section')).toBeInTheDocument()
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
expect(screen.getByRole('complementary')).toHaveClass('w-[248px]')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByRole('complementary')).toHaveClass('bg-background-body')
expect(screen.queryByRole('button', { name: 'common.mainNav.workspace.openMenu' })).not.toBeInTheDocument()
expect(screen.queryByRole('link', { name: /common.menus.roster/ })).not.toBeInTheDocument()
})
it('collapses agent detail navigation from the top-right toggle', () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
expect(screen.getByRole('complementary')).toHaveClass('w-16')
expect(screen.getByRole('complementary')).toHaveClass('p-1')
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'false')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'false')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
})
it('registers the detail navigation shortcut to run while inputs are focused', () => {
mockPathname = '/app/app-1/overview'
@ -659,21 +618,6 @@ describe('MainNav', () => {
expect.objectContaining({ ignoreInputs: false }),
)
})
it('shows agent detail navigation as a floating preview when hovering the collapsed top toggle', () => {
mockPathname = '/roster/agent/agent-1/configure'
renderMainNav()
fireEvent.click(screen.getByTestId('agent-detail-toggle'))
fireEvent.mouseEnter(screen.getByTestId('agent-detail-top').parentElement!)
expect(screen.getByRole('complementary')).toHaveClass('w-16', 'overflow-visible')
expect(localStorage.getItem('app-detail-collapse-or-expand')).toBe('collapse')
expect(screen.getAllByTestId('agent-detail-top')).toHaveLength(1)
expect(screen.getByTestId('agent-detail-top')).toHaveAttribute('data-expand', 'true')
expect(screen.getByTestId('agent-detail-section')).toHaveAttribute('data-expand', 'true')
})
it.each([
'/datasets/create',
'/datasets/create-from-pipeline',
@ -700,16 +644,6 @@ describe('MainNav', () => {
expect(marketplaceLink).toHaveClass(activeEdgeClassName)
})
it('marks roster active on roster routes', () => {
mockPathname = '/roster'
renderMainNav()
const rosterLink = screen.getByRole('link', { name: /common.menus.roster/ })
expect(rosterLink).toHaveClass(activeEdgeClassName)
expect(rosterLink).toHaveAttribute('aria-current', 'page')
})
it('applies the Figma glass active state to the Home route', () => {
mockPathname = '/'

View File

@ -17,7 +17,6 @@ import DifyLogo from '@/app/components/base/logo/dify-logo'
import EnvNav from '@/app/components/header/env-nav'
import { buildIntegrationPath } from '@/app/components/integrations/routes'
import { useAppContext } from '@/context/app-context'
import { AgentDetailSection, AgentDetailTop } from '@/features/agent-v2/agent-detail/navigation'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import Link from '@/next/link'
import { usePathname } from '@/next/navigation'
@ -61,12 +60,6 @@ const isDatasetDetailPathname = (pathname: string) => {
return true
}
const isAgentDetailPathname = (pathname: string) => {
const [section, type, agentId] = pathname.split('/').filter(Boolean)
return section === 'roster' && type === 'agent' && !!agentId
}
const MainNav = ({
className,
}: MainNavProps) => {
@ -77,8 +70,7 @@ const MainNav = ({
const showEnvTag = langGeniusVersionInfo.current_env === 'TESTING' || langGeniusVersionInfo.current_env === 'DEVELOPMENT'
const showAppDetailNavigation = !isCurrentWorkspaceDatasetOperator && pathname.startsWith('/app/')
const showDatasetDetailNavigation = isDatasetDetailPathname(pathname)
const showAgentDetailNavigation = !isCurrentWorkspaceDatasetOperator && isAgentDetailPathname(pathname)
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation || showAgentDetailNavigation
const showDetailNavigation = showAppDetailNavigation || showDatasetDetailNavigation
const { hasAppDetail, appSidebarExpand, setAppDetail, setAppSidebarExpand } = useAppStore(useShallow(state => ({
hasAppDetail: !!state.appDetail,
appSidebarExpand: state.appSidebarExpand,
@ -181,13 +173,6 @@ const MainNav = ({
icon: 'i-custom-vender-main-nav-studio',
activeIcon: 'i-custom-vender-main-nav-studio-active',
},
{
href: '/roster',
label: t('menus.roster', { ns: 'common' }),
active: (path: string) => path.startsWith('/roster'),
icon: 'i-custom-vender-main-nav-roster',
activeIcon: 'i-custom-vender-main-nav-roster-active',
},
]
: []),
...((isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator)
@ -221,27 +206,23 @@ const MainNav = ({
},
], [isCurrentWorkspaceDatasetOperator, isCurrentWorkspaceEditor, t])
const renderLogo = () => {
const appTitle = systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'
return (
<Link
href="/"
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
aria-label={appTitle}
>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-5.5 w-auto object-contain"
alt=""
/>
)
: <DifyLogo alt="" />}
</Link>
)
}
const renderLogo = () => (
<Link
href="/"
className="flex h-8 shrink-0 items-center overflow-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
aria-label={systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-5.5 w-auto object-contain"
alt=""
/>
)
: <DifyLogo alt="" />}
</Link>
)
return (
<aside
@ -280,19 +261,12 @@ const MainNav = ({
onToggle={handleToggleDetailNavigation}
/>
)
: showAgentDetailNavigation
? (
<AgentDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: (
<DatasetDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: (
<DatasetDetailTop
expand={detailNavigationVisibleExpanded}
onToggle={handleToggleDetailNavigation}
/>
)
: (
<>
<div className="flex items-center justify-between pt-3 pr-2 pb-2 pl-4">
@ -307,9 +281,7 @@ const MainNav = ({
{showDetailNavigation
? showAppDetailNavigation
? <AppDetailSection expand={detailNavigationVisibleExpanded} />
: showAgentDetailNavigation
? <AgentDetailSection expand={detailNavigationVisibleExpanded} />
: <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
: <DatasetDetailSection expand={detailNavigationVisibleExpanded} />
: (
<>
<nav className="flex flex-col gap-px p-2">

View File

@ -3,6 +3,7 @@ import type { CredentialFormSchemaBase } from '../header/account-setting/model-p
import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types'
import type { TypeWithI18N } from '@/app/components/base/form/types'
import type { Collection, ToolCredential } from '@/app/components/tools/types'
import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types'
import type { Locale } from '@/i18n-config'
export enum PluginCategoryEnum {
@ -566,12 +567,6 @@ export type StrategyDetail = {
features: AgentFeature[]
}
export const AgentFeature = {
HISTORY_MESSAGES: 'history-messages',
} as const
export type AgentFeature = typeof AgentFeature[keyof typeof AgentFeature]
type Identity = {
author: string
name: string

View File

@ -1,5 +1,4 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
@ -34,7 +33,7 @@ export const useAvailableNodesMetaData = () => {
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
return {
...node,
metaData: {
@ -45,7 +44,7 @@ export const useAvailableNodesMetaData = () => {
},
defaultValue: {
...node.defaultValue,
type: metaData.type === BlockEnum.AgentV2 ? BlockEnum.Agent : metaData.type,
type: metaData.type,
title,
},
}

View File

@ -2,7 +2,6 @@
import type { IntegrationSection } from '@/app/components/integrations/routes'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
@ -11,35 +10,23 @@ import { getMarketplaceUrl } from '@/utils/var'
type IntegrationsSettingModalProps = {
section: IntegrationSection
source?: 'agent'
onCancel: () => void
onSectionChange: (section: IntegrationSection) => void
}
export default function IntegrationsSettingModal({
section,
source,
onCancel,
onSectionChange,
}: IntegrationsSettingModalProps) {
const { t } = useTranslation()
const isAgentSource = source === 'agent'
const handleSwitchToMarketplace = useCallback((path: string) => {
window.open(getMarketplaceUrl(path), '_blank', 'noopener,noreferrer')
}, [])
return (
<MenuDialog
show
backdropClassName={isAgentSource ? 'bg-background-overlay' : undefined}
className={isAgentSource ? 'bg-transparent backdrop-blur-none' : undefined}
onClose={onCancel}
>
<div className={cn(
'mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6',
isAgentSource && 'w-full p-6',
)}
>
<MenuDialog show onClose={onCancel}>
<div className="mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6">
<div className="relative flex min-h-0 w-full shrink-0 overflow-hidden rounded-2xl border border-divider-subtle bg-components-panel-bg shadow-2xl">
<div className="fixed top-6 right-6 z-9999 flex flex-col items-center">
<Button

View File

@ -1,5 +1,5 @@
import type {
BlockDefaultValue,
PluginDefaultValue,
TriggerDefaultValue,
} from '@/app/components/workflow/block-selector/types'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
@ -98,7 +98,7 @@ const WorkflowChildren = () => {
handleOnboardingClose()
}, [handleOnboardingClose])
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => {
const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
const nodeDefault = availableNodesMetaData.nodesMap?.[nodeType]
if (!nodeDefault?.defaultValue)
return

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import type { BlockDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useTranslation } from 'react-i18next'
import { BlockEnum } from '@/app/components/workflow/types'
@ -9,7 +9,7 @@ import StartNodeSelectionPanel from './start-node-selection-panel'
type WorkflowOnboardingModalProps = {
isShow: boolean
onClose: () => void
onSelectStartNode: (nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => void
onSelectStartNode: (nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => void
}
const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import type { BlockDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { PluginDefaultValue } from '@/app/components/workflow/block-selector/types'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import NodeSelector from '@/app/components/workflow/block-selector'
@ -10,7 +10,7 @@ import StartNodeOption from './start-node-option'
type StartNodeSelectionPanelProps = {
onSelectUserInput: () => void
onSelectTrigger: (nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => void
onSelectTrigger: (nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => void
}
const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
@ -20,7 +20,7 @@ const StartNodeSelectionPanel: FC<StartNodeSelectionPanelProps> = ({
const { t } = useTranslation()
const [showTriggerSelector, setShowTriggerSelector] = useState(false)
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: BlockDefaultValue) => {
const handleTriggerSelect = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => {
setShowTriggerSelector(false)
onSelectTrigger(nodeType, toolConfig)
}, [onSelectTrigger])

View File

@ -1,6 +1,5 @@
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import type { I18nKeysWithPrefix } from '@/types/i18n'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
@ -49,7 +48,7 @@ export const useAvailableNodesMetaData = () => {
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}`, { ns: 'workflow' })
const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang
return {
...node,
@ -61,7 +60,7 @@ export const useAvailableNodesMetaData = () => {
},
defaultValue: {
...node.defaultValue,
type: metaData.type === BlockEnum.AgentV2 ? BlockEnum.Agent : metaData.type,
type: metaData.type,
title,
},
}

View File

@ -201,56 +201,6 @@ describe('CandidateNodeMain', () => {
expect(mockHandleNodeSelect).toHaveBeenCalledWith('candidate-note')
})
it('should sync draft immediately when committing an Agent v2 node', () => {
const candidateNode = createNode({
id: 'candidate-agent-v2',
type: CUSTOM_NODE,
data: {
type: BlockEnum.Agent,
title: 'Agent Candidate',
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
agent_roster: {
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
icon: 'N',
icon_background: '#E9D7FE',
icon_type: 'emoji',
role: 'Researcher',
},
version: '2',
_isCandidate: true,
},
})
render(<CandidateNodeMain candidateNode={candidateNode} />)
eventHandlers.click?.({ preventDefault: vi.fn() })
expect(mockSetNodes).toHaveBeenCalledWith(expect.arrayContaining([
expect.objectContaining({
id: 'candidate-agent-v2',
data: expect.objectContaining({
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
agent_roster: expect.objectContaining({
id: 'agent-1',
}),
version: '2',
_isCandidate: false,
}),
}),
]))
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true, true)
})
it('should append iteration and loop start helper nodes for control-flow candidates', () => {
const iterationNode = createNode({
id: 'candidate-iteration',

View File

@ -68,7 +68,6 @@ const DEFAULT_ICON_MAP: Record<BlockEnum, React.ComponentType<{ className: strin
[BlockEnum.DocExtractor]: DocsExtractor,
[BlockEnum.ListFilter]: ListFilter,
[BlockEnum.Agent]: Agent,
[BlockEnum.AgentV2]: Agent,
[BlockEnum.KnowledgeBase]: KnowledgeBase,
[BlockEnum.DataSource]: Datasource,
[BlockEnum.DataSourceEmpty]: () => null,
@ -119,7 +118,6 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
[BlockEnum.DocExtractor]: 'bg-util-colors-green-green-500',
[BlockEnum.ListFilter]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.Agent]: 'bg-util-colors-indigo-indigo-500',
[BlockEnum.AgentV2]: 'bg-util-colors-indigo-indigo-500',
[BlockEnum.HumanInput]: 'bg-util-colors-cyan-cyan-500',
[BlockEnum.KnowledgeBase]: 'bg-util-colors-warning-warning-500',
[BlockEnum.DataSource]: 'bg-components-icon-bg-midnight-solid',

View File

@ -1,10 +1,6 @@
import type { NodeDefault } from '../../types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render, screen, waitFor } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FlowType } from '@/types/common'
import { HooksStoreContext } from '../../hooks-store/provider'
import { createHooksStore } from '../../hooks-store/store'
import { BlockEnum } from '../../types'
import Blocks from '../blocks'
import { BlockClassificationEnum } from '../types'
@ -14,12 +10,6 @@ const runtimeState = vi.hoisted(() => ({
nodes: [] as Array<{ data: { type?: BlockEnum } }>,
}))
const queryMocks = vi.hoisted(() => ({
inviteOptionsQueryFn: vi.fn(),
versionDetailGet: vi.fn(),
toastError: vi.fn(),
}))
vi.mock('reactflow', () => ({
useStoreApi: () => ({
getState: () => ({
@ -36,48 +26,10 @@ vi.mock('@/app/components/app/store', () => ({
}),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
agents: {
byAgentId: {
versions: {
byVersionId: {
get: {
queryOptions: ({ input }: { input: unknown }) => ({
queryKey: ['agent-version-detail', input],
queryFn: () => queryMocks.versionDetailGet(input),
}),
},
},
},
},
inviteOptions: {
get: {
queryOptions: (options: unknown) => ({
queryKey: ['agents', 'invite-options', options],
queryFn: () => queryMocks.inviteOptionsQueryFn(options),
}),
},
},
},
},
}))
vi.mock('@langgenius/dify-ui/toast', () => ({
toast: {
error: (message: string) => queryMocks.toastError(message),
},
}))
const createBlock = (
type: BlockEnum,
title: string,
classification = BlockClassificationEnum.Default,
sort = 0,
): NodeDefault => ({
const createBlock = (type: BlockEnum, title: string, classification = BlockClassificationEnum.Default): NodeDefault => ({
metaData: {
classification,
sort,
sort: 0,
type,
title,
author: 'Dify',
@ -113,12 +65,12 @@ describe('Blocks', () => {
/>,
)
expect(screen.getByRole('button', { name: 'LLM' })).toBeInTheDocument()
expect(screen.getByText('LLM')).toBeInTheDocument()
expect(screen.getByText('Exit Loop')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.loop.loopNode')).toBeInTheDocument()
expect(screen.queryByText('Knowledge Retrieval')).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'LLM' }))
await user.click(screen.getByText('LLM'))
expect(onSelect).toHaveBeenCalledWith(BlockEnum.LLM)
})
@ -135,323 +87,4 @@ describe('Blocks', () => {
expect(screen.getByText('workflow.tabs.noResult')).toBeInTheDocument()
})
it('opens the agent selector on Agent block hover', async () => {
const user = userEvent.setup()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [],
has_more: false,
limit: 8,
page: 1,
total: 0,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={vi.fn()}
availableBlocksTypes={[BlockEnum.AgentV2]}
blocks={[createBlock(BlockEnum.AgentV2, 'Agent')]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
await user.hover(screen.getByRole('button', { name: 'Agent' }))
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
})
it('opens the agent selector from the Agent block and selects an agent', async () => {
const user = userEvent.setup()
const onSelect = vi.fn()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [
{
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
active_config_snapshot_id: 'version-1',
role: 'Researcher',
agent_kind: 'dify_agent',
icon: 'A',
icon_background: '#E9D7FE',
icon_type: 'emoji',
scope: 'roster',
source: 'workflow',
status: 'active',
},
],
has_more: false,
limit: 8,
page: 1,
total: 1,
})
queryMocks.versionDetailGet.mockResolvedValue({
config_snapshot: {
model: {
model: 'gpt-4o',
model_provider: 'openai',
},
},
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={onSelect}
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.AgentV2]}
blocks={[
createBlock(BlockEnum.LLM, 'LLM', BlockClassificationEnum.Default, 0),
createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3),
]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
expect(
screen.getByText('Agent').compareDocumentPosition(screen.getByText('LLM')) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
await user.click(screen.getByRole('button', { name: 'Agent' }))
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
expect(screen.getByRole('combobox', { name: 'agentV2.roster.searchLabel' })).toBeInTheDocument()
expect(await screen.findByText('Nadia')).toBeInTheDocument()
expect(screen.getByText('Researcher')).toBeInTheDocument()
await user.click(screen.getByRole('option', { name: 'Nadia Researcher' }))
await waitFor(() => expect(queryMocks.versionDetailGet).toHaveBeenCalledWith({
params: {
agent_id: 'agent-1',
version_id: 'version-1',
},
}))
expect(onSelect).toHaveBeenCalledWith(BlockEnum.AgentV2, {
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
agent_roster: {
description: 'Clarification Drafter',
icon: 'A',
icon_background: '#E9D7FE',
icon_type: 'emoji',
id: 'agent-1',
name: 'Nadia',
role: 'Researcher',
},
version: '2',
})
expect(queryMocks.inviteOptionsQueryFn).toHaveBeenCalledWith({
input: {
query: {
app_id: 'app-1',
limit: 8,
page: 1,
},
},
})
})
it('does not select an Agent v2 roster agent without model config', async () => {
const user = userEvent.setup()
const onSelect = vi.fn()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [
{
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
active_config_snapshot_id: 'version-1',
role: 'Researcher',
agent_kind: 'dify_agent',
icon: 'A',
icon_background: '#E9D7FE',
icon_type: 'emoji',
scope: 'roster',
source: 'workflow',
status: 'active',
},
],
has_more: false,
limit: 8,
page: 1,
total: 1,
})
queryMocks.versionDetailGet.mockResolvedValue({
config_snapshot: {},
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={onSelect}
availableBlocksTypes={[BlockEnum.AgentV2]}
blocks={[createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3)]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
await user.click(screen.getByRole('button', { name: 'Agent' }))
expect(await screen.findByText('Nadia')).toBeInTheDocument()
await user.click(screen.getByRole('option', { name: 'Nadia Researcher' }))
await waitFor(() => expect(queryMocks.toastError).toHaveBeenCalledWith('workflow.nodes.agent.modelNotSelected'))
expect(onSelect).not.toHaveBeenCalled()
})
it('inserts an inline Agent v2 node from the selector start action', async () => {
const user = userEvent.setup()
const onSelect = vi.fn()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [],
has_more: false,
limit: 8,
page: 1,
total: 0,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={onSelect}
availableBlocksTypes={[BlockEnum.AgentV2]}
blocks={[createBlock(BlockEnum.AgentV2, 'Agent', BlockClassificationEnum.Default, 3)]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
await user.click(screen.getByRole('button', { name: 'Agent' }))
await user.click(await screen.findByRole('button', { name: 'agentV2.roster.nodeSelector.startFromScratch' }))
expect(onSelect).toHaveBeenCalledWith(BlockEnum.AgentV2, {
agent_binding: {
binding_type: 'inline_agent',
},
agent_node_kind: 'dify_agent',
version: '2',
})
})
it('closes the agent selector when Escape closes the combobox', async () => {
const user = userEvent.setup()
queryMocks.inviteOptionsQueryFn.mockResolvedValue({
data: [],
has_more: false,
limit: 8,
page: 1,
total: 0,
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const hooksStore = createHooksStore({
configsMap: {
flowId: 'app-1',
flowType: FlowType.appFlow,
fileSettings: {} as never,
},
})
render(
<QueryClientProvider client={queryClient}>
<HooksStoreContext value={hooksStore}>
<Blocks
searchText=""
onSelect={vi.fn()}
availableBlocksTypes={[BlockEnum.AgentV2]}
blocks={[createBlock(BlockEnum.AgentV2, 'Agent')]}
/>
</HooksStoreContext>
</QueryClientProvider>,
)
await user.click(screen.getByRole('button', { name: 'Agent' }))
expect(await screen.findByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).toBeInTheDocument()
await user.click(screen.getByRole('combobox', { name: 'agentV2.roster.searchLabel' }))
await user.keyboard('{Escape}')
await waitFor(() => {
expect(screen.queryByRole('dialog', { name: 'agentV2.roster.nodeSelector.dialogLabel' })).not.toBeInTheDocument()
})
})
})

View File

@ -81,4 +81,22 @@ describe('block-selector hooks', () => {
expect(result.current.tabs.some(tab => tab.key === TabsEnum.Snippets)).toBe(false)
expect(result.current.activeTab).toBe(TabsEnum.Blocks)
})
it('resets the active tab to the current default tab', () => {
const { result } = renderHook(() => useTabs({
noStart: false,
}))
act(() => {
result.current.setActiveTab(TabsEnum.Start)
})
expect(result.current.activeTab).toBe(TabsEnum.Start)
act(() => {
result.current.resetActiveTab()
})
expect(result.current.activeTab).toBe(TabsEnum.Blocks)
})
})

View File

@ -127,6 +127,40 @@ describe('NodeSelector', () => {
expect(screen.getByText('End')).toBeInTheDocument()
})
it('resets to the default tab after closing', async () => {
const user = userEvent.setup()
renderNodeSelector(
<NodeSelector
onSelect={vi.fn()}
blocks={[
createBlock(BlockEnum.LLM, 'LLM'),
]}
availableBlocksTypes={[BlockEnum.LLM, BlockEnum.Start]}
showStartTab
trigger={open => (
<button type="button">
{open ? 'selector-open' : 'selector-closed'}
</button>
)}
/>,
)
await user.click(screen.getByRole('button', { name: 'selector-closed' }))
await user.click(screen.getByText('workflow.tabs.start'))
expect(screen.getByPlaceholderText('workflow.tabs.searchTrigger')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'selector-open' }))
await waitFor(() => {
expect(screen.queryByPlaceholderText('workflow.tabs.searchTrigger')).not.toBeInTheDocument()
})
await user.click(screen.getByRole('button', { name: 'selector-closed' }))
expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument()
})
it('does not open or emit open changes when disabled', async () => {
const user = userEvent.setup()
const onOpenChange = vi.fn()

View File

@ -1,346 +0,0 @@
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ComboboxRootChangeEventDetails } from '@langgenius/dify-ui/combobox'
import type { NodeDefault } from '../types'
import type { AgentRosterNodeData } from './types'
import { AvatarFallback, AvatarImage, AvatarRoot } from '@langgenius/dify-ui/avatar'
import { cn } from '@langgenius/dify-ui/cn'
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxInputGroup,
ComboboxItem,
ComboboxItemText,
ComboboxList,
ComboboxStatus,
} from '@langgenius/dify-ui/combobox'
import {
Popover,
PopoverContent,
PopoverTitle,
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
import BlockIcon from '../block-icon'
const AGENT_SELECTOR_PAGE_SIZE = 8
export function AgentSelectorContent({
open,
onOpenChange,
onSelect,
onStartFromScratch,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onSelect: (agent: AgentRosterNodeData) => void
onStartFromScratch?: () => void
}) {
const { t } = useTranslation(['agentV2', 'common', 'workflow'])
const queryClient = useQueryClient()
const appId = useHooksStore(s => s.configsMap?.flowId)
const [searchText, setSearchText] = useState('')
const [validatingAgentId, setValidatingAgentId] = useState<string>()
const debouncedSearchText = useDebounce(searchText.trim(), { wait: 300 })
const agentsQuery = useQuery({
...consoleQuery.agent.inviteOptions.get.queryOptions({
input: {
query: {
limit: AGENT_SELECTOR_PAGE_SIZE,
page: 1,
...(appId ? { app_id: appId } : {}),
...(debouncedSearchText ? { keyword: debouncedSearchText } : {}),
},
},
}),
})
const agents = agentsQuery.data?.data ?? []
const handleInputValueChange = (nextSearchText: string, details: ComboboxRootChangeEventDetails) => {
if (details.reason !== 'item-press')
setSearchText(nextSearchText)
}
const handleValueChange = async (agent: AgentInviteOptionResponse | null) => {
if (!agent)
return
if (!agent.active_config_snapshot_id) {
toast.error(t('nodes.agent.modelNotSelected', { ns: 'workflow' }))
return
}
setValidatingAgentId(agent.id)
try {
const activeConfigSnapshot = await queryClient.fetchQuery(consoleQuery.agent.byAgentId.versions.byVersionId.get.queryOptions({
input: {
params: {
agent_id: agent.id,
version_id: agent.active_config_snapshot_id,
},
},
}))
if (!activeConfigSnapshot.config_snapshot.model) {
toast.error(t('nodes.agent.modelNotSelected', { ns: 'workflow' }))
return
}
onSelect(toAgentRosterNodeData(agent))
}
catch {
toast.error(t('roster.loadingError', { ns: 'agentV2' }))
}
finally {
setValidatingAgentId(undefined)
}
}
const handleOpenChange = (nextOpen: boolean) => {
if (!nextOpen)
onOpenChange(false)
}
const isLoading = agentsQuery.isPending || !!validatingAgentId
return (
<div className="w-60 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
<Combobox<AgentInviteOptionResponse>
filter={null}
inputValue={searchText}
items={agents}
itemToStringLabel={getAgentLabel}
itemToStringValue={getAgentValue}
open={open}
value={null}
onInputValueChange={handleInputValueChange}
onOpenChange={handleOpenChange}
onValueChange={handleValueChange}
>
<div className="bg-components-panel-bg-blur p-2 pb-1">
<ComboboxInputGroup className="h-8 min-h-8 px-2">
<span aria-hidden className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder" />
<ComboboxInput
aria-label={t('roster.searchLabel', { ns: 'agentV2' })}
placeholder={t('roster.nodeSelector.searchPlaceholder', { ns: 'agentV2' })}
className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled"
/>
</ComboboxInputGroup>
</div>
<div className="max-h-54 overflow-y-auto p-1">
{isLoading && (
<AgentSelectorLoadingSkeleton label={t('loading', { ns: 'common' })} />
)}
{!isLoading && agentsQuery.isError && (
<ComboboxStatus className="px-3 py-2 system-xs-regular">
{t('roster.loadingError', { ns: 'agentV2' })}
</ComboboxStatus>
)}
{!isLoading && !agentsQuery.isError && (
<>
<ComboboxList className="max-h-none overflow-visible p-0">
{(agent: AgentInviteOptionResponse) => (
<AgentSelectorItem key={agent.id} agent={agent} />
)}
</ComboboxList>
<ComboboxEmpty className="px-3 py-2 system-xs-regular">
{debouncedSearchText
? t('roster.emptySearch', { ns: 'agentV2' })
: t('roster.empty', { ns: 'agentV2' })}
</ComboboxEmpty>
</>
)}
</div>
</Combobox>
<div className="border-t border-divider-subtle p-1">
{onStartFromScratch && (
<button
type="button"
className="flex min-h-7 w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={onStartFromScratch}
>
<span aria-hidden className="i-ri-add-line size-4 shrink-0 text-text-tertiary" />
<span className="min-w-0 flex-1 truncate">
{t('roster.nodeSelector.startFromScratch', { ns: 'agentV2' })}
</span>
</button>
)}
<Link
href="/roster"
className="flex min-h-7 w-full items-center gap-2 rounded-md px-2 py-1.5 system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
<span aria-hidden className="i-ri-arrow-right-up-line size-4 shrink-0 text-text-tertiary" />
<span className="min-w-0 flex-1 truncate">
{t('roster.nodeSelector.manageInAgentConsole', { ns: 'agentV2' })}
</span>
</Link>
</div>
</div>
)
}
function AgentSelectorLoadingSkeleton({
label,
}: {
label: string
}) {
return (
<ComboboxStatus className="p-0">
<span className="sr-only">{label}</span>
<div className="relative overflow-hidden" aria-hidden>
<div className="p-1">
{['skeleton-1', 'skeleton-2', 'skeleton-3', 'skeleton-4'].map((key, index) => (
<div
key={key}
className={cn(
'flex items-center gap-2 py-1.5 pr-3 pl-2 opacity-20',
index === 3 && 'opacity-10',
)}
>
<div className="size-8 shrink-0 rounded-full bg-text-quaternary" />
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<div className="h-2 w-20 rounded-xs bg-text-quaternary" />
<div className="h-2 w-28 rounded-xs bg-text-quaternary" />
</div>
</div>
))}
</div>
<div className="pointer-events-none absolute inset-0 bg-linear-to-b from-components-panel-bg-transparent to-background-default-subtle" />
</div>
</ComboboxStatus>
)
}
function getAgentLabel(agent: AgentInviteOptionResponse) {
return agent.name
}
function getAgentValue(agent: AgentInviteOptionResponse) {
return agent.id
}
function toAgentRosterNodeData(agent: AgentInviteOptionResponse): AgentRosterNodeData {
return {
description: agent.description,
icon: agent.icon,
icon_background: agent.icon_background,
icon_type: agent.icon_type,
id: agent.id,
name: agent.name,
role: agent.role,
}
}
function AgentSelectorAvatar({
agent,
}: {
agent: AgentInviteOptionResponse
}) {
const imageUrl = (agent.icon_type === 'image' || agent.icon_type === 'link') ? agent.icon : undefined
return (
<AvatarRoot
size="md"
className="border-[0.5px] border-divider-regular text-lg"
style={{ background: imageUrl ? undefined : (agent.icon_background || '#FFEAD5') }}
>
{imageUrl && (
<AvatarImage
src={imageUrl}
alt={agent.name}
/>
)}
<AvatarFallback size="md" className="text-lg text-text-primary-on-surface">
{agent.icon_type === 'emoji' && agent.icon ? agent.icon : agent.name[0]?.toLocaleUpperCase()}
</AvatarFallback>
</AvatarRoot>
)
}
function AgentSelectorItem({
agent,
}: {
agent: AgentInviteOptionResponse
}) {
return (
<ComboboxItem
value={agent}
className="grid-cols-[1fr] gap-0 py-1.5 pr-3 pl-2"
>
<ComboboxItemText className="flex items-center gap-2 px-0">
<span aria-hidden className="shrink-0">
<AgentSelectorAvatar agent={agent} />
</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate system-sm-medium text-text-secondary">
{agent.name}
</span>
<span className="truncate system-xs-regular text-text-tertiary">
{agent.role || agent.description}
</span>
</span>
</ComboboxItemText>
</ComboboxItem>
)
}
export function AgentBlockItem({
block,
onSelect,
onStartFromScratch,
}: {
block: NodeDefault
onSelect: (agent: AgentRosterNodeData) => void
onStartFromScratch: () => void
}) {
const { t } = useTranslation('agentV2')
const [open, setOpen] = useState(false)
const handleSelect = (agent: AgentRosterNodeData) => {
setOpen(false)
onSelect(agent)
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
openOnHover
render={(
<button
type="button"
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 text-left hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-popup-open:bg-state-base-hover"
>
<BlockIcon
className="mr-2 shrink-0"
type={block.metaData.type}
/>
<span className="min-w-0 grow truncate system-sm-medium text-text-secondary">
{block.metaData.title}
</span>
<span aria-hidden className="i-custom-vender-solid-general-arrow-down-round-fill size-4 shrink-0 -rotate-90 text-text-tertiary" />
</button>
)}
/>
<PopoverContent
placement="right-start"
sideOffset={4}
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
>
<PopoverTitle className="sr-only">
{t('roster.nodeSelector.dialogLabel')}
</PopoverTitle>
<AgentSelectorContent
open={open}
onOpenChange={setOpen}
onSelect={handleSelect}
onStartFromScratch={() => {
setOpen(false)
onStartFromScratch()
}}
/>
</PopoverContent>
</Popover>
)
}

View File

@ -1,4 +1,4 @@
import type { NodeDefault, OnSelectBlock } from '../types'
import type { NodeDefault } from '../types'
import type { BlockClassificationEnum } from './types'
import {
createPreviewCardHandle,
@ -17,13 +17,12 @@ import { useStoreApi } from 'reactflow'
import Badge from '@/app/components/base/badge'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
import { AgentBlockItem } from './agent-selector'
import { BLOCK_CLASSIFICATIONS } from './constants'
import { useBlocks } from './hooks'
type BlocksProps = {
searchText: string
onSelect: OnSelectBlock
onSelect: (type: BlockEnum) => void
availableBlocksTypes?: BlockEnum[]
blocks?: NodeDefault[]
}
@ -80,13 +79,7 @@ const Blocks = ({
const isEmpty = Object.values(groups).every(list => !list.length)
const renderGroup = useCallback((classification: BlockClassificationEnum) => {
const list = [...groups[classification]!].sort((a, b) => {
if (a.metaData.type === BlockEnum.AgentV2)
return -1
if (b.metaData.type === BlockEnum.AgentV2)
return 1
return (a.metaData.sort || 0) - (b.metaData.sort || 0)
})
const list = groups[classification]!.sort((a, b) => (a.metaData.sort || 0) - (b.metaData.sort || 0))
const { getNodes } = store.getState()
const nodes = getNodes()
const hasKnowledgeBaseNode = nodes.some(node => node.data.type === BlockEnum.KnowledgeBase)
@ -109,65 +102,39 @@ const Blocks = ({
)
}
{
filteredList.map((block) => {
if (block.metaData.type === BlockEnum.AgentV2) {
return (
<AgentBlockItem
key={block.metaData.type}
block={block}
onSelect={agent =>
onSelect(BlockEnum.AgentV2, {
agent_binding: {
binding_type: 'roster_agent',
agent_id: agent.id,
},
agent_node_kind: 'dify_agent',
agent_roster: agent,
version: '2',
})}
onStartFromScratch={() =>
onSelect(BlockEnum.AgentV2, {
agent_binding: {
binding_type: 'inline_agent',
},
agent_node_kind: 'dify_agent',
version: '2',
})}
/>
)
}
return (
<PreviewCardTrigger
key={block.metaData.type}
delay={150}
closeDelay={150}
handle={previewCardHandle}
payload={{ block }}
render={(
<button
type="button"
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 text-left hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={() => onSelect(block.metaData.type)}
>
<BlockIcon
className="mr-2 shrink-0"
type={block.metaData.type}
/>
<span className="min-w-0 grow truncate text-sm text-text-secondary">{block.metaData.title}</span>
{
block.metaData.type === BlockEnum.LoopEnd && (
<Badge
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
className="ml-2 shrink-0"
/>
)
}
</button>
)}
/>
)
})
// Preview is supplementary: icon/title/description are all reachable
// from the node that gets added on click (inspector + canvas), so
// hover/focus-only activation is a11y-safe. See
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
filteredList.map(block => (
<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
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>
)}
/>
))
}
</div>
)

View File

@ -61,16 +61,6 @@ export const ENTRY_NODE_TYPES = [
] as const
export const BLOCKS = [
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Agent,
title: 'Old Agent',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.AgentV2,
title: 'Agent',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.LLM,
@ -157,4 +147,9 @@ export const BLOCKS = [
type: BlockEnum.ListFilter,
title: 'List Filter',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Agent,
title: 'Agent',
},
] as const satisfies readonly Block[]

View File

@ -119,17 +119,21 @@ export const useTabs = ({
return fallbackTab
}, [defaultActiveTab, noBlocks, noSources, noTools, noSnippets, noStart, tabs, getValidTabKey])
const [activeTab, setActiveTab] = useState(initialTab)
const resetActiveTab = useCallback(() => {
setActiveTab(initialTab)
}, [initialTab])
useEffect(() => {
const currentTab = tabs.find(tab => tab.key === activeTab)
if (!currentTab || currentTab.disabled)
setActiveTab(initialTab)
}, [tabs, activeTab, initialTab])
resetActiveTab()
}, [tabs, activeTab, resetActiveTab])
return {
tabs,
activeTab,
setActiveTab,
resetActiveTab,
}
}

View File

@ -135,6 +135,7 @@ function NodeSelector({
const disableSnippetsTab = flowType === FlowType.snippet
const {
activeTab,
resetActiveTab,
setActiveTab,
tabs,
} = useTabs({
@ -158,6 +159,7 @@ function NodeSelector({
if (!newOpen) {
setSearchText('')
setSnippetsLoading(false)
resetActiveTab()
}
else if (activeTab === TabsEnum.Snippets) {
setSnippetsLoading(true)
@ -165,7 +167,7 @@ function NodeSelector({
if (onOpenChange)
onOpenChange(newOpen)
}, [activeTab, disabled, onOpenChange])
}, [activeTab, disabled, onOpenChange, resetActiveTab])
const handleTrigger = useCallback<MouseEventHandler<HTMLElement>>((e) => {
e.stopPropagation()
}, [])

View File

@ -1,7 +1,7 @@
'use client'
import type { OffsetOptions } from '@floating-ui/react'
import type { Placement } from '@langgenius/dify-ui/popover'
import type { ReactNode } from 'react'
import type { FC } from 'react'
import type { ToolDefaultValue, ToolValue } from './types'
import type { CustomCollectionBackend } from '@/app/components/tools/types'
import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types'
@ -14,6 +14,7 @@ import {
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
@ -36,17 +37,13 @@ import {
} from '@/service/use-tools'
type Props = Readonly<{
panelClassName?: string
disabled: boolean
trigger: ReactNode
trigger: React.ReactNode
placement?: Placement
offset?: OffsetOptions
isShow: boolean
onShowChange: (isShow: boolean) => void
}> & ToolPickerContentProps
export type ToolPickerContentProps = Readonly<{
focusSearchOnMount?: boolean
panelClassName?: string
onSelect: (tool: ToolDefaultValue) => void
onSelectMultiple: (tools: ToolDefaultValue[]) => void
supportAddCustomTool?: boolean
@ -54,18 +51,25 @@ export type ToolPickerContentProps = Readonly<{
selectedTools?: ToolValue[]
}>
export function ToolPickerContent({
focusSearchOnMount = false,
const ToolPicker: FC<Props> = ({
disabled,
trigger,
placement = 'right-start',
offset = 0,
isShow,
onShowChange,
onSelect,
onSelectMultiple,
supportAddCustomTool,
scope = 'all',
selectedTools,
panelClassName,
}: ToolPickerContentProps) {
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const sideOffset = typeof offset === 'number' ? offset : (typeof offset === 'function' ? 0 : (offset?.mainAxis ?? 0))
const alignOffset = typeof offset === 'number' ? 0 : (typeof offset === 'function' ? 0 : (offset?.crossAxis ?? 0))
const { data: enable_marketplace } = useSuspenseQuery({
...systemFeaturesQueryOptions(),
@ -116,6 +120,12 @@ export function ToolPickerContent({
const handleAddedCustomTool = invalidateCustomTools
const handleOpenChange = (nextOpen: boolean) => {
if (nextOpen && disabled)
return
onShowChange(nextOpen)
}
const handleSelect = (_type: BlockEnum, tool?: ToolDefaultValue) => {
onSelect(tool!)
}
@ -147,70 +157,6 @@ export function ToolPickerContent({
)
}
return (
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs', panelClassName)}>
<div className="p-2 pb-1">
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
placeholder={t('searchTools', { ns: 'plugin' })!}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
// The picker replaces the focused menu item inside an already-open popover.
// Focusing search keeps keyboard users in the same add-tool workflow.
/* eslint-disable-next-line jsx-a11y/no-autofocus */
autoFocus={focusSearchOnMount}
inputClassName="grow"
/>
</div>
<AllTools
className="mt-1"
toolContentClassName="max-w-full"
tags={tags}
searchText={searchText}
onSelect={handleSelect as OnSelectBlock}
onSelectMultiple={handleSelectMultiple}
buildInTools={builtinToolList || []}
customTools={customToolList || []}
workflowTools={workflowToolList || []}
mcpTools={mcpTools || []}
selectedTools={selectedTools}
onTagsChange={setTags}
featuredPlugins={featuredPlugins}
featuredLoading={isFeaturedLoading}
showFeatured={scope === 'all' && enable_marketplace}
onFeaturedInstallSuccess={async () => {
invalidateBuiltInTools()
invalidateCustomTools()
invalidateWorkflowTools()
invalidateMcpTools()
}}
/>
</div>
)
}
function ToolPicker({
disabled,
trigger,
placement = 'right-start',
offset = 0,
isShow,
onShowChange,
...contentProps
}: Props) {
const sideOffset = typeof offset === 'number' ? offset : (typeof offset === 'function' ? 0 : (offset?.mainAxis ?? 0))
const alignOffset = typeof offset === 'number' ? 0 : (typeof offset === 'function' ? 0 : (offset?.crossAxis ?? 0))
const handleOpenChange = (nextOpen: boolean) => {
if (nextOpen && disabled)
return
onShowChange(nextOpen)
}
return (
<Popover
open={isShow}
@ -229,10 +175,47 @@ function ToolPicker({
alignOffset={alignOffset}
popupClassName="border-none bg-transparent shadow-none"
>
<ToolPickerContent {...contentProps} />
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-xs', panelClassName)}>
<div className="p-2 pb-1">
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={tags}
onTagsChange={setTags}
placeholder={t('searchTools', { ns: 'plugin' })!}
supportAddCustomTool={supportAddCustomTool}
onAddedCustomTool={handleAddedCustomTool}
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
inputClassName="grow"
/>
</div>
<AllTools
className="mt-1"
toolContentClassName="max-w-full"
tags={tags}
searchText={searchText}
onSelect={handleSelect as OnSelectBlock}
onSelectMultiple={handleSelectMultiple}
buildInTools={builtinToolList || []}
customTools={customToolList || []}
workflowTools={workflowToolList || []}
mcpTools={mcpTools || []}
selectedTools={selectedTools}
onTagsChange={setTags}
featuredPlugins={featuredPlugins}
featuredLoading={isFeaturedLoading}
showFeatured={scope === 'all' && enable_marketplace}
onFeaturedInstallSuccess={async () => {
invalidateBuiltInTools()
invalidateCustomTools()
invalidateWorkflowTools()
invalidateMcpTools()
}}
/>
</div>
</PopoverContent>
</Popover>
)
}
export default ToolPicker
export default React.memo(ToolPicker)

View File

@ -85,7 +85,6 @@ const ToolItem: FC<Props> = ({
provider_id: provider.id,
provider_type: provider.type,
provider_name: provider.name,
provider_show_name: provider.label[language],
plugin_id: provider.plugin_id,
plugin_unique_identifier: provider.plugin_unique_identifier,
provider_icon: normalizedIcon,

View File

@ -118,7 +118,6 @@ const Tool: FC<Props> = ({
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
provider_show_name: payload.label[language],
plugin_id: payload.plugin_id!,
plugin_unique_identifier: payload.plugin_unique_identifier!,
provider_icon: normalizedIcon,
@ -149,7 +148,7 @@ const Tool: FC<Props> = ({
: `${selectedToolsNum} / ${totalToolsNum}`}
</span>
)
}, [actions, getIsDisabled, isAllSelected, isHovering, language, normalizedIcon, normalizedIconDark, onSelectMultiple, payload.id, payload.is_team_authorization, payload.label, payload.name, payload.plugin_id, payload.plugin_unique_identifier, payload.type, selectedToolsNum, t, totalToolsNum])
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])
if (isFoldHasSearchText !== hasSearchText) {
setIsFoldHasSearchText(hasSearchText)
@ -197,7 +196,6 @@ const Tool: FC<Props> = ({
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
provider_show_name: payload.label[language],
plugin_id: payload.plugin_id,
plugin_unique_identifier: payload.plugin_unique_identifier,
provider_icon: normalizedIcon,

View File

@ -1,4 +1,3 @@
import type { AgentInviteOptionResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ParametersSchema, PluginMeta, PluginTriggerSubscriptionConstructor, SupportedCreationMethods, TriggerEvent } from '../../plugins/types'
import type { Collection, Event } from '../../tools/types'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -49,7 +48,6 @@ export type TriggerDefaultValue = PluginCommonDefaultValue & {
}
export type ToolDefaultValue = PluginCommonDefaultValue & {
provider_show_name?: string
tool_name: string
tool_label: string
tool_description: string
@ -77,35 +75,8 @@ export type DataSourceDefaultValue = Omit<PluginCommonDefaultValue, 'provider_id
plugin_unique_identifier?: string
}
export type AgentRosterNodeData = Pick<
AgentInviteOptionResponse,
'description' | 'icon' | 'icon_background' | 'icon_type' | 'id' | 'name' | 'role'
>
export type AgentRosterBinding = {
binding_type: 'roster_agent'
agent_id: string
}
export type AgentInlineBinding = {
binding_type: 'inline_agent'
agent_id?: string | null
current_snapshot_id?: string | null
}
export type AgentBinding = AgentRosterBinding | AgentInlineBinding
export type AgentDefaultValue = {
agent_binding: AgentBinding
agent_node_kind: 'dify_agent'
agent_roster?: AgentRosterNodeData
version: '2'
}
export type PluginDefaultValue = ToolDefaultValue | DataSourceDefaultValue | TriggerDefaultValue
export type BlockDefaultValue = PluginDefaultValue | AgentDefaultValue
export type ToolValue = {
provider_name: string
provider_show_name?: string

View File

@ -17,7 +17,6 @@ import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-co
import { CUSTOM_NODE } from './constants'
import { useAutoGenerateWebhookUrl, useNodesInteractions, useNodesSyncDraft, useWorkflowHistory, WorkflowHistoryEvent } from './hooks'
import CustomNode from './nodes'
import { isAgentV2NodeData } from './nodes/agent-v2/types'
import CustomNoteNode from './note-node'
import { CUSTOM_NOTE_NODE } from './note-node/constants'
import {
@ -82,9 +81,6 @@ const CandidateNodeMain: FC<Props> = ({
onSuccess: () => autoGenerateWebhookUrl(candidateNode.id),
})
}
if (isAgentV2NodeData(candidateNode.data))
handleSyncWorkflowDraft(true, true)
})
useEventListener('contextmenu', (e) => {

View File

@ -121,22 +121,13 @@ export const SUPPORT_OUTPUT_VARS_NODE = [
BlockEnum.DocExtractor,
BlockEnum.ListFilter,
BlockEnum.Agent,
BlockEnum.AgentV2,
BlockEnum.DataSource,
BlockEnum.HumanInput,
]
export const AGENT_OUTPUT_STRUCT: Var[] = [
{
variable: 'text',
type: VarType.string,
},
{
variable: 'files',
type: VarType.arrayFile,
},
{
variable: 'json',
variable: 'usage',
type: VarType.object,
},
]

View File

@ -1,4 +1,3 @@
import agentV2Default from '@/app/components/workflow/nodes/agent-v2/default'
import agentDefault from '@/app/components/workflow/nodes/agent/default'
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
import codeDefault from '@/app/components/workflow/nodes/code/default'
@ -27,7 +26,6 @@ export const WORKFLOW_COMMON_NODES = [
llmDefault,
knowledgeRetrievalDefault,
agentDefault,
agentV2Default,
questionClassifierDefault,
ifElseDefault,
iterationDefault,

View File

@ -56,7 +56,6 @@ import {
} from '../hooks'
import { useHooksStore } from '../hooks-store/store'
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
import { isAgentV2NodeData } from '../nodes/agent-v2/types'
import { IndexMethodEnum } from '../nodes/knowledge-base/types'
import { getLLMModelIssue, isLLMModelProviderInstalled, LLMModelIssueCode } from '../nodes/llm/utils'
import {
@ -99,10 +98,6 @@ const withFlowType = (moreDataForCheckValid: CheckValidExtraData, flowType?: Flo
}
}
const getNodeMetaType = (data: CommonNodeType) => {
return isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type
}
const START_NODE_TYPES: BlockEnum[] = [
BlockEnum.Start,
BlockEnum.TriggerSchedule,
@ -254,7 +249,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
moreDataForCheckValid = getTriggerCheckParams(node!.data as PluginTriggerNodeType, triggerPlugins, language)
const toolIcon = getToolIcon(node!.data)
if (node!.data.type === BlockEnum.Agent && !isAgentV2NodeData(node!.data)) {
if (node!.data.type === BlockEnum.Agent) {
const data = node!.data as AgentNodeType
const isReadyForCheckValid = !!strategyProviders
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
@ -272,7 +267,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType?
if (node!.type === CUSTOM_NODE) {
const checkData = getCheckData(node!.data)
const validator = nodesExtraData?.[getNodeMetaType(node!.data) as BlockEnum]?.checkValid
const validator = nodesExtraData?.[node!.data.type as BlockEnum]?.checkValid
const isPluginMissing = isNodePluginMissing(node!.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList })
const errorMessages: string[] = []
@ -527,7 +522,7 @@ export const useChecklistBeforePublish = () => {
if (node!.data.type === BlockEnum.DataSource)
moreDataForCheckValid = getDataSourceCheckParams(node!.data as DataSourceNodeType, dataSourceList || [], language)
if (node!.data.type === BlockEnum.Agent && !isAgentV2NodeData(node!.data)) {
if (node!.data.type === BlockEnum.Agent) {
const data = node!.data as AgentNodeType
const isReadyForCheckValid = !!strategyProviders
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
@ -556,7 +551,7 @@ export const useChecklistBeforePublish = () => {
}
const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap)
const { errorMessage } = nodesExtraData![getNodeMetaType(node!.data) as BlockEnum].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType))
const { errorMessage } = nodesExtraData![node!.data.type as BlockEnum].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType))
if (errorMessage) {
toast.error(`[${node!.data.title}] ${errorMessage}`)

View File

@ -7,7 +7,7 @@ import type {
OnConnectStart,
ResizeParamsWithDirection,
} from 'reactflow'
import type { BlockDefaultValue } from '../block-selector/types'
import type { PluginDefaultValue } from '../block-selector/types'
import type { IterationNodeType } from '../nodes/iteration/types'
import type { LoopNodeType } from '../nodes/loop/types'
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
@ -36,7 +36,6 @@ import {
Y_OFFSET,
} from '../constants'
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
import { isAgentV2NodeData } from '../nodes/agent-v2/types'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
@ -51,7 +50,6 @@ import {
getNestedNodePosition,
getNodeCustomTypeByNodeDataType,
getNodesConnectedSourceOrTargetHandleIdsMap,
getNodesWithSameDefaultDataType,
getTopLeftNodePosition,
isClipboardEdgeStructurallyValid,
isClipboardNodeStructurallyValid,
@ -882,11 +880,13 @@ export const useNodesInteractions = () => {
return
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
const nodesWithSameType = nodes.filter(
node => node.data.type === nodeType,
)
const nodeMetaData = nodesMetaDataMap?.[nodeType]
if (!nodeMetaData)
return
const { defaultValue } = nodeMetaData
const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, nodeType, defaultValue)
const { newNode, newIterationStartNode, newLoopStartNode }
= generateNewNode({
type: getNodeCustomTypeByNodeDataType(nodeType),
@ -1439,7 +1439,7 @@ export const useNodesInteractions = () => {
currentNodeId: string,
nodeType: BlockEnum,
sourceHandle: string,
pluginDefaultValue?: BlockDefaultValue,
pluginDefaultValue?: PluginDefaultValue,
) => {
if (getNodesReadOnly())
return
@ -1447,11 +1447,13 @@ export const useNodesInteractions = () => {
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
const currentNode = nodes.find(node => node.id === currentNodeId)!
const connectedEdges = getConnectedEdges([currentNode], edges)
const nodesWithSameType = nodes.filter(
node => node.data.type === nodeType,
)
const nodeMetaData = nodesMetaDataMap?.[nodeType]
if (!nodeMetaData)
return
const { defaultValue } = nodeMetaData
const nodesWithSameType = getNodesWithSameDefaultDataType(nodes, nodeType, defaultValue)
const {
newNode: newCurrentNode,
newIterationStartNode,
@ -1727,7 +1729,7 @@ export const useNodesInteractions = () => {
if (node.type === CUSTOM_NOTE_NODE)
return true
const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum]
const nodeMeta = nodesMetaDataMap?.[node.data.type as BlockEnum]
if (!nodeMeta)
return false
@ -1739,7 +1741,7 @@ export const useNodesInteractions = () => {
if (node.type === CUSTOM_NOTE_NODE)
return {}
const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum]
const nodeMeta = nodesMetaDataMap?.[node.data.type as BlockEnum]
return nodeMeta?.defaultValue
}, [nodesMetaDataMap])

View File

@ -3,7 +3,6 @@ import type { Node } from '@/app/components/workflow/types'
import { useMemo } from 'react'
import { CollectionType } from '@/app/components/tools/types'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/types'
import { useStore } from '@/app/components/workflow/store'
import { BlockEnum } from '@/app/components/workflow/types'
import { useGetLanguage } from '@/context/i18n'
@ -33,7 +32,7 @@ export const useNodeMetaData = (node: Node) => {
const dataSourceList = useStore(s => s.dataSourceList)
const availableNodesMetaData = useNodesMetaData()
const { data } = node
const nodeMetaData = availableNodesMetaData.nodesMap?.[isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type]
const nodeMetaData = availableNodesMetaData.nodesMap?.[data.type]
const author = useMemo(() => {
if (data.type === BlockEnum.DataSource)
return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.author

View File

@ -1,5 +1,5 @@
import type { MouseEvent } from 'react'
import type { BlockDefaultValue } from '../../../block-selector/types'
import type { PluginDefaultValue } from '../../../block-selector/types'
import type { Node } from '../../../types'
import { cn } from '@langgenius/dify-ui/cn'
import {
@ -68,7 +68,7 @@ export const NodeTargetHandle = memo(({
if (!connected)
setOpen(v => !v)
}, [connected])
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: BlockDefaultValue) => {
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
handleNodeAdd(
{
nodeType: type,
@ -161,7 +161,7 @@ export const NodeSourceHandle = memo(({
e.stopPropagation()
setOpen(v => !v)
}, [])
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: BlockDefaultValue) => {
const handleSelect = useCallback((type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => {
handleNodeAdd(
{
nodeType: type,

View File

@ -1,13 +1,12 @@
import type { AgentV2NodeType } from '@/app/components/workflow/nodes/agent-v2/types'
import type { AnswerNodeType } from '@/app/components/workflow/nodes/answer/types'
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
import type { Node, PromptItem } from '@/app/components/workflow/types'
import { describe, expect, it } from 'vitest'
import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types'
import { BlockEnum, EditionType, InputVarType, PromptRole, VarType } from '@/app/components/workflow/types'
import { BlockEnum, EditionType, InputVarType, PromptRole } from '@/app/components/workflow/types'
import { AppModeEnum } from '@/types/app'
import { getNodeUsedVars, toNodeAvailableVars, updateNodeVars } from '../utils'
import { getNodeUsedVars, updateNodeVars } from '../utils'
const createNode = <T>(data: Node<T>['data']): Node<T> => ({
id: 'node-1',
@ -43,42 +42,6 @@ const createLLMNodeData = (promptTemplate: PromptItem[]): LLMNodeType => ({
})
describe('variable utils', () => {
describe('toNodeAvailableVars', () => {
it('uses Agent v2 default declared outputs for agent nodes', () => {
const node = createNode<AgentV2NodeType>({
type: BlockEnum.Agent,
title: 'Agent',
desc: '',
agent_node_kind: 'dify_agent',
version: '2',
})
const availableVars = toNodeAvailableVars({
beforeNodes: [node],
isChatMode: false,
filterVar: () => true,
allPluginInfoList: {},
})
expect(availableVars).toEqual(
expect.arrayContaining([
expect.objectContaining({
nodeId: 'node-1',
vars: [
{ variable: 'text', type: VarType.string },
{ variable: 'files', type: VarType.arrayFile },
{ variable: 'json', type: VarType.object },
],
}),
]),
)
expect(availableVars.find(item => item.nodeId === 'node-1')?.vars).not.toContainEqual({
variable: 'usage',
type: VarType.object,
})
})
})
describe('getNodeUsedVars', () => {
it('should read variables from llm jinja prompt text', () => {
const node = createNode<LLMNodeType>(
@ -145,19 +108,6 @@ describe('variable utils', () => {
]),
)
})
it('should read variables from agent task', () => {
const node = createNode<AgentV2NodeType>({
type: BlockEnum.Agent,
title: 'Agent',
desc: '',
agent_node_kind: 'dify_agent',
agent_task: 'Clarify {{#start.tender#}}',
version: '2',
})
expect(getNodeUsedVars(node)).toContainEqual(['start', 'tender'])
})
})
describe('updateNodeVars', () => {
@ -194,21 +144,6 @@ describe('variable utils', () => {
})
})
it('should replace agent task references', () => {
const node = createNode<AgentV2NodeType>({
type: BlockEnum.Agent,
title: 'Agent',
desc: '',
agent_node_kind: 'dify_agent',
agent_task: 'Clarify {{#start.tender#}}',
version: '2',
})
const updatedNode = updateNodeVars(node, ['start', 'tender'], ['start', 'question'])
expect((updatedNode.data as AgentV2NodeType).agent_task).toBe('Clarify {{#start.question#}}')
})
it('should replace human input email template references', () => {
const node = createNode<HumanInputNodeType>({
type: BlockEnum.HumanInput,

View File

@ -1,3 +1,4 @@
import type { AgentNodeType } from '../../../agent/types'
import type { AnswerNodeType } from '../../../answer/types'
import type { CodeNodeType } from '../../../code/types'
import type { DocExtractorNodeType } from '../../../document-extractor/types'
@ -13,8 +14,6 @@ import type { ParameterExtractorNodeType } from '../../../parameter-extractor/ty
import type { QuestionClassifierNodeType } from '../../../question-classifier/types'
import type { TemplateTransformNodeType } from '../../../template-transform/types'
import type { ToolNodeType } from '../../../tool/types'
import type { AgentV2NodeType } from '@/app/components/workflow/nodes/agent-v2/types'
import type { AgentNodeType } from '@/app/components/workflow/nodes/agent/types'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { CaseItem, Condition } from '@/app/components/workflow/nodes/if-else/types'
@ -52,7 +51,6 @@ import {
TEMPLATE_TRANSFORM_OUTPUT_STRUCT,
TOOL_OUTPUT_STRUCT,
} from '@/app/components/workflow/constants'
import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/types'
import DataSourceNodeDefault from '@/app/components/workflow/nodes/data-source/default'
import HumanInputNodeDefault from '@/app/components/workflow/nodes/human-input/default'
import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types'
@ -594,11 +592,6 @@ const formatItem = (
}
case BlockEnum.Agent: {
if (isAgentV2NodeData(data)) {
res.vars = AGENT_OUTPUT_STRUCT
break
}
const payload = data as AgentNodeType
const outputs: Var[] = []
Object.keys(payload.output_schema?.properties || {}).forEach(
@ -617,11 +610,6 @@ const formatItem = (
break
}
case BlockEnum.AgentV2: {
res.vars = AGENT_OUTPUT_STRUCT
break
}
case BlockEnum.DataSource: {
const payload = data as DataSourceNodeType
const dataSourceVars
@ -1460,31 +1448,6 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
res = [...(mixVars as ValueSelector[]), ...(vars as any)]
break
}
case BlockEnum.Agent: {
if (isAgentV2NodeData(data)) {
const payload = data as AgentV2NodeType
res = matchNotSystemVars([payload.agent_task || ''])
break
}
const payload = data as AgentNodeType
const valueSelectors: ValueSelector[] = []
if (!payload.agent_parameters)
break
Object.keys(payload.agent_parameters || {}).forEach((key) => {
const { value } = payload.agent_parameters![key]!
if (typeof value === 'string')
valueSelectors.push(...matchNotSystemVars([value]))
})
res = valueSelectors
break
}
case BlockEnum.AgentV2: {
const payload = data as AgentV2NodeType
res = matchNotSystemVars([payload.agent_task || ''])
break
}
case BlockEnum.DataSource: {
const payload = data as DataSourceNodeType
const mixVars = matchNotSystemVars(
@ -1544,6 +1507,21 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
break
}
case BlockEnum.Agent: {
const payload = data as AgentNodeType
const valueSelectors: ValueSelector[] = []
if (!payload.agent_parameters)
break
Object.keys(payload.agent_parameters || {}).forEach((key) => {
const { value } = payload.agent_parameters![key]!
if (typeof value === 'string')
valueSelectors.push(...matchNotSystemVars([value]))
})
res = valueSelectors
break
}
case BlockEnum.HumanInput: {
const payload = data as HumanInputNodeType
const formContent = payload.form_content
@ -1904,17 +1882,43 @@ export const updateNodeVars = (
}
break
}
case BlockEnum.Agent: {
if (isAgentV2NodeData(data)) {
const payload = data as AgentV2NodeType
payload.agent_task = replaceOldVarInText(
payload.agent_task || '',
oldVarSelector,
newVarSelector,
)
break
}
case BlockEnum.DataSource: {
const payload = data as DataSourceNodeType
const hasShouldRenameVar = Object.keys(
payload.datasource_parameters,
)?.filter(
key =>
payload.datasource_parameters[key]!.type !== ToolVarType.constant,
)
if (hasShouldRenameVar) {
Object.keys(payload.datasource_parameters).forEach((key) => {
const value = payload.datasource_parameters[key]!
const { type } = value!
if (
type === ToolVarType.variable
&& value!.value.join('.') === oldVarSelector.join('.')
) {
payload.datasource_parameters[key] = {
...value,
value: newVarSelector,
}
}
if (type === ToolVarType.mixed) {
payload.datasource_parameters[key] = {
...value,
value: replaceOldVarInText(
payload.datasource_parameters[key]!.value as string,
oldVarSelector,
newVarSelector,
),
}
}
})
}
break
}
case BlockEnum.Agent: {
const payload = data as AgentNodeType
if (payload.agent_parameters) {
Object.keys(payload.agent_parameters).forEach((key) => {
@ -1954,51 +1958,6 @@ export const updateNodeVars = (
}
break
}
case BlockEnum.AgentV2: {
const payload = data as AgentV2NodeType
payload.agent_task = replaceOldVarInText(
payload.agent_task || '',
oldVarSelector,
newVarSelector,
)
break
}
case BlockEnum.DataSource: {
const payload = data as DataSourceNodeType
const hasShouldRenameVar = Object.keys(
payload.datasource_parameters,
)?.filter(
key =>
payload.datasource_parameters[key]!.type !== ToolVarType.constant,
)
if (hasShouldRenameVar) {
Object.keys(payload.datasource_parameters).forEach((key) => {
const value = payload.datasource_parameters[key]!
const { type } = value!
if (
type === ToolVarType.variable
&& value!.value.join('.') === oldVarSelector.join('.')
) {
payload.datasource_parameters[key] = {
...value,
value: newVarSelector,
}
}
if (type === ToolVarType.mixed) {
payload.datasource_parameters[key] = {
...value,
value: replaceOldVarInText(
payload.datasource_parameters[key]!.value as string,
oldVarSelector,
newVarSelector,
),
}
}
})
}
break
}
case BlockEnum.VariableAssigner: {
const payload = data as VariableAssignerNodeType
if (payload.variables) {

View File

@ -28,14 +28,6 @@ const mockLogsState = {
showSpecialResultPanel: false,
}
const createMockSingleRunParams = () => ({
forms: [],
onStop: vi.fn(),
runningStatus: NodeRunningStatus.Succeeded,
existVarValuesInForms: [],
filteredExistVarForms: [],
})
const mockLastRunState = {
isShowSingleRun: false,
hideSingleRun: vi.fn(),
@ -51,7 +43,13 @@ const mockLastRunState = {
setIsRunAfterSingleRun: vi.fn(),
setTabType: vi.fn(),
handleAfterCustomSingleRun: vi.fn(),
singleRunParams: createMockSingleRunParams(),
singleRunParams: {
forms: [],
onStop: vi.fn(),
runningStatus: NodeRunningStatus.Succeeded,
existVarValuesInForms: [],
filteredExistVarForms: [],
},
nodeInfo: { id: 'node-1' },
setRunInputData: vi.fn(),
handleStop: () => mockHandleStop(),
@ -308,7 +306,6 @@ describe('workflow-panel index', () => {
mockLogsState.showSpecialResultPanel = false
mockLastRunState.isShowSingleRun = false
mockLastRunState.tabType = 'settings'
mockLastRunState.singleRunParams = createMockSingleRunParams()
})
it('should render the settings panel and wire title, description, run, and close actions', async () => {
@ -463,26 +460,6 @@ describe('workflow-panel index', () => {
expect(screen.getByText('data-source-before-run-form')).toBeInTheDocument()
})
it('should keep the settings panel visible when single-run params have no forms', () => {
mockLastRunState.isShowSingleRun = true
mockLastRunState.singleRunParams = {} as ReturnType<typeof createMockSingleRunParams>
renderWorkflowComponent(
<BasePanel id="node-agent" data={createData({ type: BlockEnum.Agent }) as never}>
<div>panel-child</div>
</BasePanel>,
{
initialStoreState: {
nodePanelWidth: 480,
otherPanelWidth: 200,
},
},
)
expect(screen.queryByText('before-run-form')).not.toBeInTheDocument()
expect(screen.getByText('panel-child')).toBeInTheDocument()
})
it('should render data source authorization controls and jump to the settings modal', () => {
renderWorkflowComponent(
<BasePanel id="node-1" data={createData({ type: BlockEnum.DataSource, plugin_id: 'source-1', provider_type: 'remote' }) as never}>

View File

@ -1,4 +1,4 @@
import type { CSSProperties, FC, ReactNode } from 'react'
import type { FC, ReactNode } from 'react'
import type { SimpleSubscription } from '@/app/components/plugins/plugin-detail-panel/subscription-list'
import type { Node } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
@ -58,7 +58,6 @@ import { useHooksStore } from '@/app/components/workflow/hooks-store'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import { NodeActionsDropdown } from '@/app/components/workflow/node-actions-menu'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/types'
import { useLogs } from '@/app/components/workflow/run/hooks'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { useStore } from '@/app/components/workflow/store'
@ -230,7 +229,6 @@ const BasePanel: FC<BasePanelProps> = ({
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
const isChildNode = !!(data.isInIteration || data.isInLoop)
const nodeMetaType = isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type
const isSupportSingleRun = canRunBySingle(data.type, isChildNode)
const appDetail = useAppStore(state => state.appDetail)
@ -295,7 +293,7 @@ const BasePanel: FC<BasePanelProps> = ({
flowId: configsMap?.flowId || '',
flowType: configsMap?.flowType || FlowType.appFlow,
data,
defaultRunInputData: nodesMap?.[nodeMetaType]?.defaultRunInputData || {},
defaultRunInputData: nodesMap?.[data.type]?.defaultRunInputData || {},
isPaused,
})
@ -407,11 +405,6 @@ const BasePanel: FC<BasePanelProps> = ({
id,
data,
}) as Node, [id, data])
const singleRunForms = singleRunParams?.forms
const isCustomRunFormNode = isSupportCustomRunForm(data.type)
const shouldRenderSingleRunPanel = isShowSingleRun && (isCustomRunFormNode || !!singleRunForms)
const isSingleRunPanelVisible = showSingleRunPanel && shouldRenderSingleRunPanel
if (logParams.showSpecialResultPanel) {
return (
<div className={cn(
@ -420,7 +413,7 @@ const BasePanel: FC<BasePanelProps> = ({
>
<div
ref={containerRef}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', isSingleRunPanelVisible ? 'overflow-hidden' : 'overflow-y-auto')}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
style={{
width: `${nodePanelWidth}px`,
}}
@ -438,39 +431,20 @@ const BasePanel: FC<BasePanelProps> = ({
)
}
if (shouldRenderSingleRunPanel) {
const customRunForm = isCustomRunFormNode
? getCustomRunForm({
nodeId: id,
flowId: configsMap?.flowId || '',
flowType: configsMap?.flowType || FlowType.appFlow,
payload: data,
setRunResult,
setIsRunAfterSingleRun,
isPaused,
isRunAfterSingleRun,
onSuccess: handleAfterCustomSingleRun,
onCancel: hideSingleRun,
appendNodeInspectVars,
})
: null
const singleRunPanelContent = isCustomRunFormNode
? customRunForm
: singleRunForms
? (
<BeforeRunForm
nodeName={data.title}
nodeType={data.type}
onHide={hideSingleRun}
onRun={handleRunWithParams}
{...singleRunParams!}
{...passedLogParams}
existVarValuesInForms={getExistVarValuesInForms(singleRunForms)}
filteredExistVarForms={getFilteredExistVarForms(singleRunForms)}
handleAfterHumanInputStepRun={handleAfterCustomSingleRun}
/>
)
: null
if (isShowSingleRun) {
const form = getCustomRunForm({
nodeId: id,
flowId: configsMap?.flowId || '',
flowType: configsMap?.flowType || FlowType.appFlow,
payload: data,
setRunResult,
setIsRunAfterSingleRun,
isPaused,
isRunAfterSingleRun,
onSuccess: handleAfterCustomSingleRun,
onCancel: hideSingleRun,
appendNodeInspectVars,
})
return (
<div className={cn(
@ -479,12 +453,29 @@ const BasePanel: FC<BasePanelProps> = ({
>
<div
ref={containerRef}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', isSingleRunPanelVisible ? 'overflow-hidden' : 'overflow-y-auto')}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
style={{
width: `${nodePanelWidth}px`,
}}
>
{singleRunPanelContent}
{isSupportCustomRunForm(data.type)
? (
form
)
: (
<BeforeRunForm
nodeName={data.title}
nodeType={data.type}
onHide={hideSingleRun}
onRun={handleRunWithParams}
{...singleRunParams!}
{...passedLogParams}
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
handleAfterHumanInputStepRun={handleAfterCustomSingleRun}
/>
)}
</div>
</div>
)
@ -494,9 +485,6 @@ const BasePanel: FC<BasePanelProps> = ({
const singleRunActionLabel = isSingleRunning
? t('debug.variableInspect.trigger.stop', { ns: 'workflow' })
: runThisStepLabel
const nodePanelRightOffset = !showMessageLogModal
? '4px'
: `${otherPanelWidth + 8}px`
const isStartPlaceholderPanel = data.type === BlockEnum.StartPlaceholder
const panelChildren = cloneElement(children as any, {
id,
@ -529,9 +517,8 @@ const BasePanel: FC<BasePanelProps> = ({
showMessageLogModal && 'absolute z-0 mr-2 w-[400px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
)}
style={{
'right': !showMessageLogModal ? '0' : `${otherPanelWidth}px`,
'--workflow-node-panel-right': nodePanelRightOffset,
} as CSSProperties}
right: !showMessageLogModal ? '0' : `${otherPanelWidth}px`,
}}
>
<div
ref={triggerRef}
@ -543,11 +530,10 @@ const BasePanel: FC<BasePanelProps> = ({
ref={containerRef}
value={tabType}
onValueChange={selectedValue => setTabType(selectedValue)}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg transition-[width] ease-linear', isSingleRunPanelVisible ? 'overflow-hidden' : 'overflow-y-auto')}
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg transition-[width] ease-linear', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
style={{
'width': `${nodePanelWidth}px`,
'--workflow-node-panel-width': `${nodePanelWidth}px`,
} as CSSProperties}
width: `${nodePanelWidth}px`,
}}
>
<div className="sticky top-0 z-10 shrink-0 border-b-[0.5px] border-divider-regular bg-components-panel-bg">
<div className="flex items-center px-4 pt-4 pb-1">
@ -609,7 +595,7 @@ const BasePanel: FC<BasePanelProps> = ({
</Tooltip>
)
}
<HelpLink nodeType={nodeMetaType} />
<HelpLink nodeType={data.type} />
<NodeActionsDropdown id={id} data={data} showHelpLink={false} />
<div className="mx-3 h-3.5 w-px bg-divider-regular" />
<button

View File

@ -10,6 +10,7 @@ import {
import { useWorkflowRunValidation } from '@/app/components/workflow/hooks/use-checklist'
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
import useAgentSingleRunFormParams from '@/app/components/workflow/nodes/agent/use-single-run-form-params'
import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params'
import useCodeSingleRunFormParams from '@/app/components/workflow/nodes/code/use-single-run-form-params'
import useDocExtractorSingleRunFormParams from '@/app/components/workflow/nodes/document-extractor/use-single-run-form-params'
@ -49,8 +50,7 @@ const singleRunFormParamsHooks: Record<BlockEnum, any> = {
[BlockEnum.Tool]: useToolSingleRunFormParams,
[BlockEnum.ParameterExtractor]: useParameterExtractorSingleRunFormParams,
[BlockEnum.Iteration]: useIterationSingleRunFormParams,
[BlockEnum.Agent]: undefined,
[BlockEnum.AgentV2]: undefined,
[BlockEnum.Agent]: useAgentSingleRunFormParams,
[BlockEnum.DocExtractor]: useDocExtractorSingleRunFormParams,
[BlockEnum.Loop]: useLoopSingleRunFormParams,
[BlockEnum.Start]: useStartSingleRunFormParams,
@ -91,7 +91,6 @@ const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
[BlockEnum.ParameterExtractor]: undefined,
[BlockEnum.Iteration]: undefined,
[BlockEnum.Agent]: undefined,
[BlockEnum.AgentV2]: undefined,
[BlockEnum.DocExtractor]: undefined,
[BlockEnum.Loop]: undefined,
[BlockEnum.Start]: undefined,

View File

@ -1,114 +0,0 @@
import type { AgentV2NodeType } from '../types'
import { BlockEnum } from '@/app/components/workflow/types'
import nodeDefault from '../default'
import { isAgentV2NodeData } from '../types'
const t = vi.fn((key: string, options?: Record<string, unknown>) => {
if (key === 'errorMsg.fieldRequired')
return `required:${options?.field}`
if (key === 'nodes.agent.roster.label')
return 'Agent'
return key
})
const createPayload = (overrides: Partial<AgentV2NodeType> = {}): AgentV2NodeType => ({
title: 'Agent',
desc: '',
type: BlockEnum.AgentV2,
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
agent_roster: {
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
icon: 'N',
icon_background: '#E9D7FE',
icon_type: 'emoji',
role: 'Researcher',
},
version: '2',
...overrides,
})
describe('agent/default', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('requires a selected roster agent', () => {
const result = nodeDefault.checkValid(createPayload({ agent_roster: undefined }), t)
expect(result).toEqual({
isValid: false,
errorMessage: 'required:Agent',
})
})
it('requires a roster agent binding', () => {
const result = nodeDefault.checkValid(createPayload({ agent_binding: undefined }), t)
expect(result).toEqual({
isValid: false,
errorMessage: 'required:Agent',
})
})
it('requires the roster agent binding to match the selected roster agent', () => {
const result = nodeDefault.checkValid(createPayload({
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-2',
},
}), t)
expect(result).toEqual({
isValid: false,
errorMessage: 'required:Agent',
})
})
it('passes validation when a roster agent is selected', () => {
const result = nodeDefault.checkValid(createPayload(), t)
expect(result).toEqual({
isValid: true,
errorMessage: '',
})
})
it('passes validation for inline agent binding', () => {
const result = nodeDefault.checkValid(createPayload({
agent_binding: {
binding_type: 'inline_agent',
},
agent_roster: undefined,
}), t)
expect(result).toEqual({
isValid: true,
errorMessage: '',
})
})
it('creates Agent v2 graph data by default', () => {
expect(nodeDefault.defaultValue).toMatchObject({
agent_node_kind: 'dify_agent',
version: '2',
})
})
it('identifies version 2 agent data as Agent v2', () => {
expect(isAgentV2NodeData(createPayload({ type: BlockEnum.Agent }))).toBe(true)
expect(isAgentV2NodeData({
title: 'Old Agent',
desc: '',
type: BlockEnum.Agent,
version: '2',
} as Parameters<typeof isAgentV2NodeData>[0])).toBe(false)
})
})

View File

@ -1,114 +0,0 @@
import type { ReactNode } from 'react'
import type { AgentV2NodeType } from '../types'
import { render, screen } from '@testing-library/react'
import { BlockEnum } from '@/app/components/workflow/types'
import { AgentV2Node } from '../node'
vi.mock('../../_base/components/setting-item', () => ({
SettingItem: ({
label,
status,
tooltip,
children,
}: {
label: ReactNode
status?: string
tooltip?: string
children?: ReactNode
}) => (
<div>
{`${label}:${status || 'normal'}:${tooltip || ''}`}
{children}
</div>
),
}))
const createData = (overrides: Partial<AgentV2NodeType> = {}): AgentV2NodeType => ({
title: 'Agent',
desc: '',
type: BlockEnum.AgentV2,
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
agent_roster: {
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
icon: 'N',
icon_background: '#E9D7FE',
icon_type: 'emoji',
role: 'Researcher',
},
version: '2',
...overrides,
})
describe('agent/node', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('renders the selected roster agent', () => {
const { container } = render(
<AgentV2Node
id="agent-node"
data={createData()}
/>,
)
expect(screen.getByText('workflow.nodes.agent.roster.label')).toHaveClass('px-2.5', 'py-0.5', 'system-2xs-medium-uppercase')
expect(screen.getByText('Nadia')).toHaveClass('system-xs-regular', 'text-text-secondary')
expect(screen.getByText('Researcher')).toHaveClass('system-2xs-regular', 'text-text-tertiary')
expect(container.querySelector('.bg-workflow-block-parma-bg')).toHaveClass('gap-1', 'rounded-lg', 'p-1')
expect(container.querySelector('.h-1.px-3')).not.toBeInTheDocument()
})
it('renders the inline setup agent style', () => {
const { container } = render(
<AgentV2Node
id="agent-node"
data={createData({
agent_binding: {
binding_type: 'inline_agent',
},
agent_roster: undefined,
})}
/>,
)
expect(screen.getByText('workflow.nodes.agent.roster.inlineSetup.name')).toHaveClass('system-xs-regular', 'text-text-secondary')
expect(screen.getByText('workflow.nodes.agent.roster.inlineSetup.type')).toHaveClass('system-2xs-regular', 'text-text-tertiary')
const robotIcon = container.querySelector('.i-custom-vender-agent-v2-robot-3')
expect(robotIcon).toHaveClass('size-5')
expect(robotIcon?.parentElement).toHaveClass('size-8', 'rounded-full', 'bg-background-default-burn')
})
it('renders an error state when no roster agent is selected', () => {
render(
<AgentV2Node
id="agent-node"
data={createData({ agent_roster: undefined })}
/>,
)
expect(screen.getByText(/workflow.nodes.agent.roster.label:error:/)).toHaveTextContent('workflow.errorMsg.fieldRequired')
})
it('renders an error state when the roster binding does not match the selected agent', () => {
render(
<AgentV2Node
id="agent-node"
data={createData({
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-2',
},
})}
/>,
)
expect(screen.getByText(/workflow.nodes.agent.roster.label:error:/)).toHaveTextContent('Nadia')
})
})

View File

@ -1,458 +0,0 @@
import type { ReactNode } from 'react'
import type { AgentV2NodeType } from '../types'
import type { PromptEditorProps } from '@/app/components/base/prompt-editor'
import type { NodePanelProps } from '@/app/components/workflow/types'
import { fireEvent, render, screen, within } from '@testing-library/react'
import { BlockEnum } from '@/app/components/workflow/types'
import { AgentV2Panel } from '../panel'
const {
mockEditorFocus,
mockEditorUpdate,
mockHandleNodeDataUpdateWithSyncDraft,
mockInvalidateQueries,
mockInsertNodes,
mockPromptEditorProps,
mockSetInputs,
mockUseComposerQuery,
mockUseNodeCrud,
} = vi.hoisted(() => ({
mockEditorFocus: vi.fn(),
mockEditorUpdate: vi.fn((callback: () => void) => callback()),
mockHandleNodeDataUpdateWithSyncDraft: vi.fn((_payload, options) => options?.callback?.onSuccess?.()),
mockInvalidateQueries: vi.fn(),
mockInsertNodes: vi.fn(),
mockPromptEditorProps: [] as PromptEditorProps[],
mockSetInputs: vi.fn(),
mockUseComposerQuery: vi.fn(),
mockUseNodeCrud: vi.fn(),
}))
vi.mock('@tanstack/react-query', () => ({
skipToken: Symbol('skipToken'),
useQuery: () => mockUseComposerQuery(),
useQueryClient: () => ({
invalidateQueries: mockInvalidateQueries,
}),
}))
vi.mock('@/service/client', () => ({
consoleQuery: {
apps: {
byAppId: {
workflows: {
draft: {
nodes: {
byNodeId: {
agentComposer: {
get: {
queryKey: (input: unknown) => ['workflow-agent-composer', input],
queryOptions: (options: unknown) => ({
queryKey: ['workflow-agent-composer', options],
}),
},
},
},
},
},
},
},
},
},
}))
vi.mock('@/app/components/workflow/hooks-store', () => ({
useHooksStore: (selector: (state: { configsMap: { flowId: string } }) => unknown) => selector({
configsMap: {
flowId: 'app-1',
},
}),
}))
vi.mock('../../_base/components/output-vars', () => ({
__esModule: true,
default: ({ children }: { children: ReactNode }) => <div>{children}</div>,
VarItem: ({ name, type, description }: { name: string, type: string, description?: string }) => (
<div>{`${name}:${type}:${description || ''}`}</div>
),
}))
vi.mock('@/app/components/base/prompt-editor', () => ({
default: (props: PromptEditorProps) => {
mockPromptEditorProps.push(props)
return (
<>
<textarea
aria-label="workflow.nodes.agent.task.label"
placeholder={typeof props.placeholder === 'string' ? props.placeholder : undefined}
value={props.value}
onChange={event => props.onChange?.(event.currentTarget.value)}
/>
{props.children}
</>
)
},
}))
vi.mock('@lexical/react/LexicalComposerContext', () => ({
useLexicalComposerContext: () => [{
focus: mockEditorFocus,
update: mockEditorUpdate,
}],
}))
vi.mock('lexical', async (importOriginal) => {
const actual = await importOriginal<typeof import('lexical')>()
return {
...actual,
$insertNodes: mockInsertNodes,
}
})
vi.mock('@/app/components/base/prompt-editor/plugins/custom-text/node', () => ({
$createCustomTextNode: (text: string) => ({
getTextContent: () => text,
}),
}))
vi.mock('../../_base/hooks/use-node-crud', () => ({
default: (id: string, data: AgentV2NodeType) => mockUseNodeCrud(id, data),
}))
vi.mock('@/app/components/workflow/block-selector/agent-selector', () => ({
AgentSelectorContent: ({
onSelect,
}: {
onSelect: (agent: NonNullable<AgentV2NodeType['agent_roster']>) => void
}) => (
<button
type="button"
onClick={() => onSelect({
id: 'agent-2',
name: 'Mara',
description: 'Tender Analyst',
icon: 'M',
icon_background: '#D1E9FF',
icon_type: 'emoji',
role: 'Analyst',
})}
>
Select Mara
</button>
),
}))
vi.mock('../../_base/hooks/use-available-var-list', () => ({
default: () => ({
availableVars: [{
nodeId: 'start',
title: 'START',
vars: [{ variable: 'question', type: 'string' }],
}],
availableNodesWithParent: [{
id: 'start',
data: {
title: 'START',
type: BlockEnum.Start,
},
position: { x: 0, y: 0 },
}],
}),
}))
vi.mock('@/app/components/workflow/hooks', () => ({
useNodeDataUpdate: () => ({
handleNodeDataUpdateWithSyncDraft: mockHandleNodeDataUpdateWithSyncDraft,
}),
useWorkflowVariableType: () => vi.fn(),
}))
const createData = (overrides: Partial<AgentV2NodeType> = {}): AgentV2NodeType => ({
title: 'Agent',
desc: '',
type: BlockEnum.AgentV2,
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-1',
},
agent_node_kind: 'dify_agent',
agent_roster: {
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
icon: 'N',
icon_background: '#E9D7FE',
icon_type: 'emoji',
role: 'Researcher',
},
version: '2',
...overrides,
})
const panelProps = {} as NodePanelProps<AgentV2NodeType>['panelProps']
const createComposerState = (overrides: Record<string, unknown> = {}) => ({
node_job: {
schema_version: 1,
mode: 'tell_agent_what_to_do',
workflow_prompt: 'Composer task',
previous_node_output_refs: [],
declared_outputs: [],
human_contacts: [],
metadata: {},
},
effective_declared_outputs: [
{
name: 'text',
type: 'string',
required: false,
description: 'Free-form text answer.',
},
{
name: 'files',
type: 'array',
required: false,
description: 'Files produced by the agent.',
array_item: {
type: 'file',
},
},
{
name: 'json',
type: 'object',
required: false,
description: 'Free-form JSON object.',
},
],
...overrides,
})
describe('agent/panel', () => {
beforeEach(() => {
vi.clearAllMocks()
mockPromptEditorProps.length = 0
mockUseComposerQuery.mockReturnValue({
data: createComposerState(),
})
mockUseNodeCrud.mockImplementation((_id: string, data: AgentV2NodeType) => ({
inputs: data,
setInputs: mockSetInputs,
}))
})
it('renders selected roster agent trigger and default Agent v2 output vars', () => {
render(
<AgentV2Panel
id="agent-node"
data={createData()}
panelProps={panelProps}
/>,
)
expect(screen.getByText('workflow.nodes.agent.roster.label')).toBeInTheDocument()
expect(screen.getByText('Nadia')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.agent.task.label')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'workflow.nodes.agent.task.tooltip' })).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: 'workflow.nodes.agent.task.label' })).toHaveValue('')
expect(screen.getByRole('button', { name: 'workflow.nodes.agent.advancedSetting' })).toBeInTheDocument()
expect(screen.getByText('text:String:workflow.nodes.agent.outputVars.text')).toBeInTheDocument()
expect(screen.queryByText('usage:object:workflow.nodes.agent.outputVars.usage')).not.toBeInTheDocument()
expect(screen.getByText('files:Array[File]:workflow.nodes.agent.outputVars.files.title')).toBeInTheDocument()
expect(screen.getByText('json:Object:workflow.nodes.agent.outputVars.json')).toBeInTheDocument()
})
it('opens and closes the roster agent layered panel', () => {
render(
<AgentV2Panel
id="agent-node"
data={createData()}
panelProps={panelProps}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /^workflow\.nodes\.agent\.roster\.openPanel/ }))
const panel = screen.getByRole('dialog', { name: 'Nadia' })
expect(panel).toBeInTheDocument()
expect(within(panel).getByText('Researcher')).toBeInTheDocument()
expect(within(panel).getByRole('link', { name: 'workflow.nodes.agent.roster.editInConsole' })).toHaveAttribute('href', '/roster/agent/agent-1/configure')
expect(within(panel).getByRole('button', { name: 'workflow.nodes.agent.roster.makeCopy' })).toBeInTheDocument()
fireEvent.keyDown(panel, { key: 'Escape' })
expect(screen.queryByRole('dialog', { name: 'Nadia' })).not.toBeInTheDocument()
})
it('renders a required roster state when no roster agent is selected', () => {
render(
<AgentV2Panel
id="agent-node"
data={createData({ agent_roster: undefined })}
panelProps={panelProps}
/>,
)
expect(screen.getByText('workflow.nodes.agent.roster.label')).toBeInTheDocument()
expect(screen.getByText(/^workflow\.errorMsg\.fieldRequired/)).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.agent.task.label')).toBeInTheDocument()
expect(screen.getByText('text:String:workflow.nodes.agent.outputVars.text')).toBeInTheDocument()
})
it('updates roster agent binding from the selector', () => {
render(
<AgentV2Panel
id="agent-node"
data={createData()}
panelProps={panelProps}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'workflow.nodes.agent.roster.change' }))
fireEvent.click(screen.getByRole('button', { name: 'Select Mara' }))
expect(mockHandleNodeDataUpdateWithSyncDraft).toHaveBeenCalledWith(
{
id: 'agent-node',
data: expect.objectContaining({
agent_binding: {
binding_type: 'roster_agent',
agent_id: 'agent-2',
},
agent_roster: expect.objectContaining({
id: 'agent-2',
name: 'Mara',
role: 'Analyst',
}),
}),
},
expect.objectContaining({
sync: true,
notRefreshWhenSyncError: true,
}),
)
expect(mockInvalidateQueries).toHaveBeenCalled()
})
it('does not fall back to the roster agent description when role is empty', () => {
render(
<AgentV2Panel
id="agent-node"
data={createData({
agent_roster: {
id: 'agent-1',
name: 'Nadia',
description: 'Clarification Drafter',
icon: 'N',
icon_background: '#E9D7FE',
icon_type: 'emoji',
role: '',
},
})}
panelProps={panelProps}
/>,
)
expect(screen.getByText('Nadia')).toBeInTheDocument()
expect(screen.queryByText('Clarification Drafter')).not.toBeInTheDocument()
})
it('updates agent task and opens prompt insertion shortcuts', () => {
mockUseComposerQuery.mockReturnValue({
data: createComposerState({
node_job: {
schema_version: 1,
mode: 'tell_agent_what_to_do',
workflow_prompt: 'Composer task',
previous_node_output_refs: [],
declared_outputs: [],
human_contacts: [],
metadata: {},
},
}),
})
render(
<AgentV2Panel
id="agent-node"
data={createData({ agent_task: 'Graph task' })}
panelProps={panelProps}
/>,
)
const editor = screen.getByRole('textbox', { name: 'workflow.nodes.agent.task.label' })
expect(editor).toHaveValue('Graph task')
fireEvent.change(editor, { target: { value: 'Clarify {{#start.question#}}' } })
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
agent_task: 'Clarify {{#start.question#}}',
}))
expect(mockPromptEditorProps[0]?.workflowVariableBlock).toMatchObject({
show: true,
})
expect(mockPromptEditorProps[0]?.contextBlock).toBeUndefined()
fireEvent.click(screen.getByRole('button', { name: 'workflow.nodes.agent.task.insert' }))
expect(mockEditorFocus).toHaveBeenCalled()
expect(mockInsertNodes.mock.calls[0]?.[0]?.[0]?.getTextContent()).toBe('/')
fireEvent.click(screen.getByRole('button', { name: 'workflow.nodes.agent.task.mention' }))
expect(mockInsertNodes.mock.calls[1]?.[0]?.[0]?.getTextContent()).toBe('{')
})
it('saves agent task to workflow draft node data', () => {
render(
<AgentV2Panel
id="agent-node"
data={createData()}
panelProps={panelProps}
/>,
)
fireEvent.change(screen.getByRole('textbox', { name: 'workflow.nodes.agent.task.label' }), {
target: {
value: 'Use the previous result',
},
})
expect(mockSetInputs).toHaveBeenCalledWith(expect.objectContaining({
agent_task: 'Use the previous result',
}))
})
it('renders effective declared outputs from the workflow composer until outputs are graph-backed', () => {
mockUseComposerQuery.mockReturnValue({
data: createComposerState({
effective_declared_outputs: [
{
name: 'summary',
type: 'string',
description: 'Short summary',
},
{
name: 'attachments',
type: 'array',
description: 'Generated files',
array_item: {
type: 'file',
},
},
],
}),
})
render(
<AgentV2Panel
id="agent-node"
data={createData()}
panelProps={panelProps}
/>,
)
expect(screen.getByText('summary:String:Short summary')).toBeInTheDocument()
expect(screen.getByText('attachments:Array[File]:Generated files')).toBeInTheDocument()
expect(screen.queryByText('text:String:workflow.nodes.agent.outputVars.text')).not.toBeInTheDocument()
})
})

View File

@ -1,27 +0,0 @@
import {
CollapsiblePanel,
CollapsibleRoot,
CollapsibleTrigger,
} from '@langgenius/dify-ui/collapsible'
import { useTranslation } from 'react-i18next'
export function AgentAdvancedSettings() {
const { t } = useTranslation()
return (
<CollapsibleRoot className="border-b border-divider-subtle py-2">
<CollapsibleTrigger className="group h-8 min-h-0 justify-start gap-0 rounded-none px-4 py-0 hover:not-data-disabled:bg-transparent hover:not-data-disabled:text-text-secondary data-panel-open:text-text-secondary">
<span className="min-w-0 truncate system-sm-semibold-uppercase text-text-secondary">
{t('nodes.agent.advancedSetting', { ns: 'workflow' })}
</span>
<span
aria-hidden="true"
className="i-custom-vender-solid-general-arrow-down-round-fill size-4 rotate-270 text-text-quaternary transition-transform group-data-panel-open:rotate-0 motion-reduce:transition-none"
/>
</CollapsibleTrigger>
<CollapsiblePanel>
<div className="px-4" />
</CollapsiblePanel>
</CollapsibleRoot>
)
}

View File

@ -1,259 +0,0 @@
import type { RefObject } from 'react'
import type { AgentRosterNodeData } from '@/app/components/workflow/block-selector/types'
import { AvatarFallback, AvatarImage, AvatarRoot } from '@langgenius/dify-ui/avatar'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
Drawer,
DrawerCloseButton,
DrawerContent,
DrawerPopup,
DrawerPortal,
DrawerTitle,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import {
Popover,
PopoverContent,
PopoverTitle,
PopoverTrigger,
} from '@langgenius/dify-ui/popover'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AgentSelectorContent } from '@/app/components/workflow/block-selector/agent-selector'
import { getAgentDetailPath } from '@/features/agent-v2/agent-detail/routes'
import Link from '@/next/link'
const i18nPrefix = 'nodes.agent'
function AgentRosterAvatar({
agent,
size = 'lg',
className,
}: {
agent: AgentRosterNodeData
size?: 'xs' | 'md' | 'lg'
className?: string
}) {
const imageUrl = (agent.icon_type === 'image' || agent.icon_type === 'link') ? agent.icon : undefined
return (
<AvatarRoot
size={size}
className={cn('border-[0.5px] border-divider-regular', className)}
style={{ background: imageUrl ? undefined : (agent.icon_background || '#FFEAD5') }}
>
{imageUrl && (
<AvatarImage
src={imageUrl}
alt={agent.name}
/>
)}
<AvatarFallback size={size} className="text-text-primary-on-surface">
{agent.icon_type === 'emoji' && agent.icon ? agent.icon : agent.name[0]?.toLocaleUpperCase()}
</AvatarFallback>
</AvatarRoot>
)
}
function AgentRosterDrawer({
agent,
open,
portalContainerRef,
onClose,
}: {
agent: AgentRosterNodeData
open: boolean
portalContainerRef: RefObject<HTMLDivElement | null>
onClose: () => void
}) {
const { t } = useTranslation()
return (
<Drawer
open={open}
modal={false}
disablePointerDismissal
swipeDirection="right"
onOpenChange={(nextOpen) => {
if (!nextOpen)
onClose()
}}
>
<DrawerPortal container={portalContainerRef}>
<DrawerViewport className="pointer-events-none">
<DrawerPopup
className="pointer-events-auto p-0 data-[swipe-direction=right]:top-14 data-[swipe-direction=right]:bottom-1 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-r-[0.5px] data-[swipe-direction=right]:border-divider-subtle data-[swipe-direction=right]:shadow-2xl data-[swipe-direction=right]:shadow-shadow-shadow-5"
style={{
right: 'var(--workflow-node-panel-right, 4px)',
width: 'var(--workflow-node-panel-width, 400px)',
}}
>
<DrawerContent className="flex min-h-0 flex-1 flex-col overflow-hidden p-0 pb-0">
<header className="flex h-[108px] shrink-0 flex-col gap-3 border-b border-divider-subtle bg-components-panel-bg py-3 pr-4 pl-3">
<div className="flex h-10 min-w-0 items-start justify-between">
<div className="flex h-10 min-w-0 flex-1 items-center gap-2 px-0.5 py-0.5">
<AgentRosterAvatar agent={agent} size="md" className="size-9" />
<div className="flex min-w-0 flex-1 flex-col gap-0.5 py-px">
<div className="flex min-w-0 items-center gap-1">
<DrawerTitle className="truncate system-sm-medium text-text-secondary">
{agent.name}
</DrawerTitle>
<span aria-hidden className="i-ri-lock-line size-3 shrink-0 text-text-tertiary" />
</div>
<p className="truncate system-xs-regular text-text-tertiary">
{agent.role}
</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-1 py-1">
<button
type="button"
aria-label={t(`${i18nPrefix}.roster.more`, { ns: 'workflow' })}
className="flex size-6 cursor-pointer items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
<span aria-hidden className="i-ri-more-fill size-4" />
</button>
<div className="flex h-3.5 items-start px-1">
<div className="h-full w-px bg-divider-regular" />
</div>
<DrawerCloseButton
aria-label={t('operation.close', { ns: 'common' })}
className="size-6 rounded-md"
/>
</div>
</div>
<div className="flex h-8 gap-2 pl-1">
<Link
href={getAgentDetailPath(agent.id, 'configure')}
className="inline-flex h-8 min-w-0 flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 text-[13px] leading-4 font-medium whitespace-nowrap text-components-button-secondary-text shadow-xs outline-hidden backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
<span aria-hidden className="i-ri-external-link-line size-4 shrink-0" />
<span className="truncate">
{t(`${i18nPrefix}.roster.editInConsole`, { ns: 'workflow' })}
</span>
</Link>
<Button
variant="secondary"
size="medium"
className="min-w-0 flex-1 gap-1.5 px-3"
>
<span aria-hidden className="i-ri-file-copy-2-line size-4 shrink-0" />
<span className="truncate">
{t(`${i18nPrefix}.roster.makeCopy`, { ns: 'workflow' })}
</span>
</Button>
</div>
</header>
<div
role="region"
aria-label={t(`${i18nPrefix}.roster.panelLabel`, { ns: 'workflow', name: agent.name })}
className="min-h-0 flex-1 overflow-y-auto overscroll-contain"
>
<div className="h-full min-h-80 bg-components-panel-bg" />
</div>
</DrawerContent>
</DrawerPopup>
</DrawerViewport>
</DrawerPortal>
</Drawer>
)
}
export function AgentRosterField({
agent,
portalContainerRef,
onChange,
}: {
agent?: AgentRosterNodeData
portalContainerRef: RefObject<HTMLDivElement | null>
onChange: (agent: AgentRosterNodeData) => void
}) {
const { t } = useTranslation()
const [isPanelOpen, setIsPanelOpen] = useState(false)
const [isSelectorOpen, setIsSelectorOpen] = useState(false)
const rosterRequiredMessage = t('errorMsg.fieldRequired', {
ns: 'workflow',
field: t(`${i18nPrefix}.roster.label`, { ns: 'workflow' }),
})
return (
<FieldRoot name="agent_roster" className="gap-1 px-4 py-2">
<div className="flex h-6 items-center gap-2">
<FieldLabel className="min-w-0 flex-1 py-1 system-sm-semibold-uppercase! text-text-secondary">
{t('nodes.agent.roster.label', { ns: 'workflow' })}
</FieldLabel>
<Popover open={isSelectorOpen} onOpenChange={setIsSelectorOpen}>
<PopoverTrigger
render={(
<button
type="button"
className="flex h-6 shrink-0 cursor-pointer items-center justify-center rounded-md px-1.5 py-1 system-xs-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
{t(`${i18nPrefix}.roster.change`, { ns: 'workflow' })}
</button>
)}
/>
<PopoverContent
placement="bottom-end"
sideOffset={4}
popupClassName="border-none bg-transparent p-0 shadow-none backdrop-blur-none"
>
<PopoverTitle className="sr-only">
{t('roster.nodeSelector.dialogLabel', { ns: 'agentV2' })}
</PopoverTitle>
<AgentSelectorContent
open={isSelectorOpen}
onOpenChange={setIsSelectorOpen}
onSelect={(nextAgent) => {
setIsSelectorOpen(false)
onChange(nextAgent)
}}
/>
</PopoverContent>
</Popover>
</div>
{agent
? (
<>
<button
type="button"
aria-label={t(`${i18nPrefix}.roster.openPanel`, { ns: 'workflow', name: agent.name })}
className="flex h-13 w-full min-w-0 cursor-pointer items-center gap-2 rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg py-2 pr-4 pl-2 text-left shadow-xs shadow-shadow-shadow-3 hover:bg-components-panel-on-panel-item-bg-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
onClick={() => setIsPanelOpen(true)}
>
<AgentRosterAvatar agent={agent} />
<span className="flex min-w-0 flex-1 flex-col gap-0.5 py-px">
<span className="truncate system-sm-medium text-text-secondary">
{agent.name}
</span>
<span className="truncate system-xs-regular text-text-tertiary">
{agent.role}
</span>
</span>
<span className="flex shrink-0 items-center text-text-tertiary">
<span aria-hidden className="i-ri-arrow-right-line size-4" />
</span>
</button>
<AgentRosterDrawer
agent={agent}
open={isPanelOpen}
portalContainerRef={portalContainerRef}
onClose={() => setIsPanelOpen(false)}
/>
</>
)
: (
<div className="flex h-13 w-full min-w-0 items-center gap-2 rounded-[10px] border-[0.5px] border-state-destructive-border bg-components-panel-on-panel-item-bg py-2 pr-4 pl-2 text-left shadow-xs shadow-shadow-shadow-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-state-destructive-hover text-text-destructive">
<span aria-hidden className="i-ri-error-warning-line size-4" />
</span>
<span className="min-w-0 flex-1 truncate system-sm-medium text-text-destructive">
{rosterRequiredMessage}
</span>
</div>
)}
</FieldRoot>
)
}

Some files were not shown because too many files have changed in this diff Show More