Compare commits
3 Commits
deploy/saa
...
fix/workfl
| Author | SHA1 | Date | |
|---|---|---|---|
| 449ea15941 | |||
| de2ec990d8 | |||
| 2b7f5ab982 |
18
AGENTS.md
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = ""
|
||||
@ -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 == []
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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}/`)
|
||||
|
||||
|
||||
@ -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" />
|
||||
}
|
||||
@ -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" />
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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" />
|
||||
}
|
||||
@ -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" />
|
||||
}
|
||||
@ -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`)
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import RosterPage from '@/features/agent-v2/roster/page'
|
||||
|
||||
export default function Page() {
|
||||
return <RosterPage />
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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} />
|
||||
)}
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
|
||||
{t('settings.provider', { ns: 'common' })}
|
||||
|
||||
</button>
|
||||
|
||||
</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>
|
||||
|
||||
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
@ -1,4 +0,0 @@
|
||||
import type { RosterReferenceBlockType } from '../../types'
|
||||
import { createContext } from 'react'
|
||||
|
||||
export const RosterReferenceBlockContext = createContext<RosterReferenceBlockType | undefined>(undefined)
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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 })
|
||||
})
|
||||
})
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -247,7 +247,6 @@ export type DefaultModelResponse = {
|
||||
export type DefaultModel = {
|
||||
provider: string
|
||||
model: string
|
||||
plugin_id?: string
|
||||
}
|
||||
|
||||
export type CustomConfigurationModelFixedFields = {
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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])
|
||||
}
|
||||
|
||||
@ -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 = '/'
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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> = ({
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}, [])
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||