From 5f7eb7bde9c5b80bc358f3ee0562c2eaeafc47d0 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Mon, 25 May 2026 14:26:19 +0800 Subject: [PATCH 1/6] feat: add workflow_version to workflow_agent_node_bindings (#36603) Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/clients/agent_backend/request_builder.py | 9 ++- ...add_workflow_version_to_workflow_agent_.py | 65 +++++++++++++++++++ api/models/agent.py | 14 +++- api/services/agent/composer_service.py | 11 ++++ .../agent/workflow_publish_service.py | 2 + .../layers/pydantic_ai/history.py | 4 +- .../src/dify_agent/layers/output/configs.py | 1 + .../dify_agent/layers/output/output_layer.py | 2 + .../local/dify_agent/runtime/test_runner.py | 24 +++++-- 9 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 api/migrations/versions/2026_05_25_1143-97e2e1a644e8_add_workflow_version_to_workflow_agent_.py diff --git a/api/clients/agent_backend/request_builder.py b/api/clients/agent_backend/request_builder.py index a886fe849f..392eee641b 100644 --- a/api/clients/agent_backend/request_builder.py +++ b/api/clients/agent_backend/request_builder.py @@ -55,10 +55,14 @@ class AgentBackendModelConfig(BaseModel): class AgentBackendOutputConfig(BaseModel): - """API-side structured output declaration for the conventional output layer.""" + """API-side structured output declaration for the conventional output layer. + + The structured-output tool name is fixed to ``final_output`` inside + ``dify_agent.layers.output`` so callers only control the JSON Schema plus + optional description/strictness metadata. + """ json_schema: dict[str, JsonValue] - name: str = "final_result" description: str | None = None strict: bool | None = None @@ -153,7 +157,6 @@ class AgentBackendRunRequestBuilder: metadata=run_input.metadata, config=DifyOutputLayerConfig( json_schema=run_input.output.json_schema, - name=run_input.output.name, description=run_input.output.description, strict=run_input.output.strict, ), diff --git a/api/migrations/versions/2026_05_25_1143-97e2e1a644e8_add_workflow_version_to_workflow_agent_.py b/api/migrations/versions/2026_05_25_1143-97e2e1a644e8_add_workflow_version_to_workflow_agent_.py new file mode 100644 index 0000000000..7348e19b3c --- /dev/null +++ b/api/migrations/versions/2026_05_25_1143-97e2e1a644e8_add_workflow_version_to_workflow_agent_.py @@ -0,0 +1,65 @@ +"""add workflow_version to workflow_agent_node_bindings + +Restores the stage 1 §5.3 unique key +``(tenant_id, workflow_id, workflow_version, node_id)`` so draft and published +workflow bindings can coexist at the same workflow_id once we want to track +them per workflow version. ``workflow_version`` mirrors ``workflows.version`` +("draft" or a published version string). + +Because the New Agent Experience feature is pre-release, this table is empty +in every environment that matters; the ``server_default='draft'`` only exists +to keep developer-local rows valid during the alter and is dropped immediately +afterward so application code must specify ``workflow_version`` explicitly. + +Revision ID: 97e2e1a644e8 +Revises: f8b6b7e9c421 +Create Date: 2026-05-25 11:43:37.611300 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '97e2e1a644e8' +down_revision = 'f8b6b7e9c421' +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table('workflow_agent_node_bindings', schema=None) as batch_op: + batch_op.add_column( + sa.Column( + 'workflow_version', + sa.String(length=255), + nullable=False, + server_default='draft', + ) + ) + batch_op.alter_column('workflow_version', server_default=None) + batch_op.drop_constraint( + batch_op.f('workflow_agent_node_binding_node_unique'), type_='unique' + ) + batch_op.create_unique_constraint( + 'workflow_agent_node_binding_node_version_unique', + ['tenant_id', 'workflow_id', 'workflow_version', 'node_id'], + ) + batch_op.create_index( + 'workflow_agent_node_binding_workflow_version_idx', + ['tenant_id', 'workflow_id', 'workflow_version'], + unique=False, + ) + + +def downgrade(): + with op.batch_alter_table('workflow_agent_node_bindings', schema=None) as batch_op: + batch_op.drop_index('workflow_agent_node_binding_workflow_version_idx') + batch_op.drop_constraint( + 'workflow_agent_node_binding_node_version_unique', type_='unique' + ) + batch_op.create_unique_constraint( + batch_op.f('workflow_agent_node_binding_node_unique'), + ['tenant_id', 'workflow_id', 'node_id'], + postgresql_nulls_not_distinct=False, + ) + batch_op.drop_column('workflow_version') diff --git a/api/models/agent.py b/api/models/agent.py index 15ad423bab..a8f048eef5 100644 --- a/api/models/agent.py +++ b/api/models/agent.py @@ -231,17 +231,29 @@ class WorkflowAgentNodeBinding(DefaultFieldsMixin, Base): UniqueConstraint( "tenant_id", "workflow_id", + "workflow_version", "node_id", - name="workflow_agent_node_binding_node_unique", + name="workflow_agent_node_binding_node_version_unique", ), Index("workflow_agent_node_binding_agent_idx", "tenant_id", "agent_id"), Index("workflow_agent_node_binding_current_snapshot_idx", "tenant_id", "current_snapshot_id"), Index("workflow_agent_node_binding_app_idx", "tenant_id", "app_id"), + Index( + "workflow_agent_node_binding_workflow_version_idx", + "tenant_id", + "workflow_id", + "workflow_version", + ), ) tenant_id: Mapped[str] = mapped_column(StringUUID, nullable=False) app_id: Mapped[str] = mapped_column(StringUUID, nullable=False) workflow_id: Mapped[str] = mapped_column(StringUUID, nullable=False) + # Tracks which workflow version (draft or a published version string) this + # binding belongs to. Mirrors ``Workflow.version`` and lets us keep separate + # rows for the draft workflow and each published copy under the same + # workflow_id, restoring the stage 1 §5.3 unique key. + workflow_version: Mapped[str] = mapped_column(String(255), nullable=False) node_id: Mapped[str] = mapped_column(String(255), nullable=False) binding_type: Mapped[WorkflowAgentBindingType] = mapped_column( EnumText(WorkflowAgentBindingType, length=32), nullable=False diff --git a/api/services/agent/composer_service.py b/api/services/agent/composer_service.py index c1b396cb82..27f3771408 100644 --- a/api/services/agent/composer_service.py +++ b/api/services/agent/composer_service.py @@ -28,6 +28,10 @@ from services.entities.agent_entities import ( WorkflowNodeJobConfig, ) +# WorkflowAgentNodeBinding.workflow_version tag for the draft workflow row. +# Mirrors Workflow.version when it is "draft" (see models/workflow.py). +_DRAFT_WORKFLOW_VERSION = "draft" + class AgentComposerService: @classmethod @@ -284,6 +288,7 @@ class AgentComposerService: tenant_id=tenant_id, app_id=app_id, workflow_id=workflow_id, + workflow_version=_DRAFT_WORKFLOW_VERSION, node_id=node_id, binding_type=WorkflowAgentBindingType.INLINE_AGENT, agent_id=agent.id, @@ -387,6 +392,7 @@ class AgentComposerService: tenant_id=tenant_id, app_id=app_id, workflow_id=workflow_id, + workflow_version=_DRAFT_WORKFLOW_VERSION, node_id=node_id, created_by=account_id, ) @@ -606,11 +612,16 @@ class AgentComposerService: def _get_workflow_binding( cls, *, tenant_id: str, workflow_id: str, node_id: str ) -> WorkflowAgentNodeBinding | None: + # Composer always operates against the draft workflow row, so this lookup + # is scoped to ``workflow_version="draft"``. Published bindings are + # materialized by WorkflowAgentPublishService.copy_agent_node_bindings_to_published + # and are not edited through the Composer. return db.session.scalar( select(WorkflowAgentNodeBinding) .where( WorkflowAgentNodeBinding.tenant_id == tenant_id, WorkflowAgentNodeBinding.workflow_id == workflow_id, + WorkflowAgentNodeBinding.workflow_version == _DRAFT_WORKFLOW_VERSION, WorkflowAgentNodeBinding.node_id == node_id, ) .limit(1) diff --git a/api/services/agent/workflow_publish_service.py b/api/services/agent/workflow_publish_service.py index 06985dc3fa..af3e511229 100644 --- a/api/services/agent/workflow_publish_service.py +++ b/api/services/agent/workflow_publish_service.py @@ -39,6 +39,7 @@ class WorkflowAgentPublishService: WorkflowAgentNodeBinding.tenant_id == draft_workflow.tenant_id, WorkflowAgentNodeBinding.app_id == draft_workflow.app_id, WorkflowAgentNodeBinding.workflow_id == draft_workflow.id, + WorkflowAgentNodeBinding.workflow_version == draft_workflow.version, WorkflowAgentNodeBinding.node_id.in_(node_ids), ) ).all() @@ -48,6 +49,7 @@ class WorkflowAgentPublishService: tenant_id=binding.tenant_id, app_id=binding.app_id, workflow_id=published_workflow.id, + workflow_version=published_workflow.version, node_id=binding.node_id, binding_type=binding.binding_type, agent_id=binding.agent_id, diff --git a/dify-agent/src/agenton_collections/layers/pydantic_ai/history.py b/dify-agent/src/agenton_collections/layers/pydantic_ai/history.py index 80aa3076bb..ac91e4006a 100644 --- a/dify-agent/src/agenton_collections/layers/pydantic_ai/history.py +++ b/dify-agent/src/agenton_collections/layers/pydantic_ai/history.py @@ -31,9 +31,7 @@ class PydanticAIHistoryRuntimeState(BaseModel): model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", validate_assignment=True) -class PydanticAIHistoryLayer( - PydanticAILayer[NoLayerDeps, object, EmptyLayerConfig, PydanticAIHistoryRuntimeState] -): +class PydanticAIHistoryLayer(PydanticAILayer[NoLayerDeps, object, EmptyLayerConfig, PydanticAIHistoryRuntimeState]): """State-only layer that stores pydantic-ai message history. The mutable history lives only in ``runtime_state.messages``. Helper methods diff --git a/dify-agent/src/dify_agent/layers/output/configs.py b/dify-agent/src/dify_agent/layers/output/configs.py index 719d8a38c1..0d73ef5411 100644 --- a/dify-agent/src/dify_agent/layers/output/configs.py +++ b/dify-agent/src/dify_agent/layers/output/configs.py @@ -47,4 +47,5 @@ class DifyOutputLayerConfig(LayerConfig): raise ValueError("Schema must declare an object output.") return value + __all__ = ["DIFY_OUTPUT_LAYER_TYPE_ID", "DifyOutputLayerConfig"] diff --git a/dify-agent/src/dify_agent/layers/output/output_layer.py b/dify-agent/src/dify_agent/layers/output/output_layer.py index 4667fdd308..206f0cceb2 100644 --- a/dify-agent/src/dify_agent/layers/output/output_layer.py +++ b/dify-agent/src/dify_agent/layers/output/output_layer.py @@ -187,6 +187,8 @@ def _build_exposed_json_schema( if description is not None: exposed_schema["description"] = description return exposed_schema + + def _reject_non_local_refs(schema: JsonValue) -> None: """Reject references that would require external fetching or non-local state. diff --git a/dify-agent/tests/local/dify_agent/runtime/test_runner.py b/dify-agent/tests/local/dify_agent/runtime/test_runner.py index 361b2b7cfa..ddf860beb6 100644 --- a/dify-agent/tests/local/dify_agent/runtime/test_runner.py +++ b/dify-agent/tests/local/dify_agent/runtime/test_runner.py @@ -5,7 +5,15 @@ from typing import Any import httpx import pytest from pydantic_ai.exceptions import UnexpectedModelBehavior -from pydantic_ai.messages import ModelMessage, ModelRequest, ModelResponse, SystemPromptPart, TextPart, ToolCallPart, UserPromptPart +from pydantic_ai.messages import ( + ModelMessage, + ModelRequest, + ModelResponse, + SystemPromptPart, + TextPart, + ToolCallPart, + UserPromptPart, +) from pydantic_ai.models import ModelRequestParameters from pydantic_ai.models.test import TestModel from pydantic_ai.settings import ModelSettings @@ -163,11 +171,15 @@ def _history_session_snapshot( runtime_state=PydanticAIHistoryRuntimeState(messages=messages).model_dump(mode="json"), ), LayerSessionSnapshot(name="plugin", lifecycle_state=LifecycleState.SUSPENDED, runtime_state={}), - LayerSessionSnapshot(name=DIFY_AGENT_MODEL_LAYER_ID, lifecycle_state=LifecycleState.SUSPENDED, runtime_state={}), + LayerSessionSnapshot( + name=DIFY_AGENT_MODEL_LAYER_ID, lifecycle_state=LifecycleState.SUSPENDED, runtime_state={} + ), ] if include_output: layers.append( - LayerSessionSnapshot(name=DIFY_AGENT_OUTPUT_LAYER_ID, lifecycle_state=LifecycleState.SUSPENDED, runtime_state={}) + LayerSessionSnapshot( + name=DIFY_AGENT_OUTPUT_LAYER_ID, lifecycle_state=LifecycleState.SUSPENDED, runtime_state={} + ) ) return CompositorSessionSnapshot(layers=layers) @@ -257,7 +269,11 @@ def test_runner_passes_temporary_system_prompt_prefix_without_history_layer(monk assert request_parts[1].content == "current user" terminal = sink.events["run-no-history"][-1] assert isinstance(terminal, RunSucceededEvent) - assert [layer.name for layer in terminal.data.session_snapshot.layers] == ["prompt", "plugin", DIFY_AGENT_MODEL_LAYER_ID] + assert [layer.name for layer in terminal.data.session_snapshot.layers] == [ + "prompt", + "plugin", + DIFY_AGENT_MODEL_LAYER_ID, + ] def test_runner_prepends_current_system_prompt_to_stored_history_and_appends_only_new_messages( From e617435d034c2ce955a183cc9af9b938f0d9c5de Mon Sep 17 00:00:00 2001 From: Cocoon-Break <54054995+kuishou68@users.noreply.github.com> Date: Mon, 25 May 2026 15:15:24 +0800 Subject: [PATCH 2/6] fix: replace .distinct() with .group_by(Conversation.id) for PostgreSQL JSON compatibility (#36610) Co-authored-by: cocoon Co-authored-by: Asuka Minato Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/app/conversation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/controllers/console/app/conversation.py b/api/controllers/console/app/conversation.py index 6216a7bcbe..0ca7a08286 100644 --- a/api/controllers/console/app/conversation.py +++ b/api/controllers/console/app/conversation.py @@ -134,7 +134,7 @@ class CompletionConversationApi(Resource): .join( # type: ignore MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id ) - .distinct() + .group_by(Conversation.id) ) elif args.annotation_status == "not_annotated": query = ( @@ -272,7 +272,7 @@ class ChatConversationApi(Resource): .join( # type: ignore MessageAnnotation, MessageAnnotation.conversation_id == Conversation.id ) - .distinct() + .group_by(Conversation.id) ) case "not_annotated": query = ( From 345ba80942902038bf8aaef17dddad5849b2706e Mon Sep 17 00:00:00 2001 From: Lillian <11332799+Lillian68@users.noreply.github.com> Date: Mon, 25 May 2026 15:33:32 +0800 Subject: [PATCH 3/6] fix: type mismatches (route says uuid: but handler says str) (#36612) --- api/controllers/console/app/message.py | 2 +- .../console/app/workflow_draft_variable.py | 29 ++-- api/controllers/console/app/workflow_run.py | 2 +- .../console/datasets/datasets_document.py | 4 +- .../rag_pipeline_draft_variable.py | 37 ++--- .../rag_pipeline/rag_pipeline_workflow.py | 2 +- api/controllers/service_api/app/annotation.py | 8 +- .../service_api/app/file_preview.py | 7 +- .../rag_pipeline/rag_pipeline_workflow.py | 22 +-- .../service_api/dataset/segment.py | 130 ++++++++++++------ 10 files changed, 149 insertions(+), 94 deletions(-) diff --git a/api/controllers/console/app/message.py b/api/controllers/console/app/message.py index faa1e0fcda..7445fed86c 100644 --- a/api/controllers/console/app/message.py +++ b/api/controllers/console/app/message.py @@ -417,7 +417,7 @@ class MessageApi(Resource): @setup_required @login_required @account_initialization_required - def get(self, app_model, message_id: str): + def get(self, app_model, message_id: UUID): message_id_str = str(message_id) message = db.session.scalar( diff --git a/api/controllers/console/app/workflow_draft_variable.py b/api/controllers/console/app/workflow_draft_variable.py index 3c887c33dc..83f0d1dde6 100644 --- a/api/controllers/console/app/workflow_draft_variable.py +++ b/api/controllers/console/app/workflow_draft_variable.py @@ -2,6 +2,7 @@ import logging from collections.abc import Callable from functools import wraps from typing import Any, TypedDict +from uuid import UUID from flask import Response, request from flask_restx import Resource, fields, marshal, marshal_with @@ -345,14 +346,15 @@ class VariableApi(Resource): @console_ns.response(404, "Variable not found") @_api_prerequisite @marshal_with(workflow_draft_variable_model) - def get(self, app_model: App, variable_id: str): + def get(self, app_model: App, variable_id: UUID): draft_var_srv = WorkflowDraftVariableService( session=db.session(), ) + variable_id_str = str(variable_id) variable = _ensure_variable_access( - variable=draft_var_srv.get_variable(variable_id=variable_id), + variable=draft_var_srv.get_variable(variable_id=variable_id_str), app_id=app_model.id, - variable_id=variable_id, + variable_id=variable_id_str, ) return variable @@ -363,7 +365,7 @@ class VariableApi(Resource): @console_ns.response(404, "Variable not found") @_api_prerequisite @marshal_with(workflow_draft_variable_model) - def patch(self, app_model: App, variable_id: str): + def patch(self, app_model: App, variable_id: UUID): # Request payload for file types: # # Local File: @@ -390,10 +392,11 @@ class VariableApi(Resource): ) args_model = WorkflowDraftVariableUpdatePayload.model_validate(console_ns.payload or {}) + variable_id_str = str(variable_id) variable = _ensure_variable_access( - variable=draft_var_srv.get_variable(variable_id=variable_id), + variable=draft_var_srv.get_variable(variable_id=variable_id_str), app_id=app_model.id, - variable_id=variable_id, + variable_id=variable_id_str, ) new_name = args_model.name @@ -434,14 +437,15 @@ class VariableApi(Resource): @console_ns.response(204, "Variable deleted successfully") @console_ns.response(404, "Variable not found") @_api_prerequisite - def delete(self, app_model: App, variable_id: str): + def delete(self, app_model: App, variable_id: UUID): draft_var_srv = WorkflowDraftVariableService( session=db.session(), ) + variable_id_str = str(variable_id) variable = _ensure_variable_access( - variable=draft_var_srv.get_variable(variable_id=variable_id), + variable=draft_var_srv.get_variable(variable_id=variable_id_str), app_id=app_model.id, - variable_id=variable_id, + variable_id=variable_id_str, ) draft_var_srv.delete_variable(variable) db.session.commit() @@ -457,7 +461,7 @@ class VariableResetApi(Resource): @console_ns.response(204, "Variable reset (no content)") @console_ns.response(404, "Variable not found") @_api_prerequisite - def put(self, app_model: App, variable_id: str): + def put(self, app_model: App, variable_id: UUID): draft_var_srv = WorkflowDraftVariableService( session=db.session(), ) @@ -468,10 +472,11 @@ class VariableResetApi(Resource): raise NotFoundError( f"Draft workflow not found, app_id={app_model.id}", ) + variable_id_str = str(variable_id) variable = _ensure_variable_access( - variable=draft_var_srv.get_variable(variable_id=variable_id), + variable=draft_var_srv.get_variable(variable_id=variable_id_str), app_id=app_model.id, - variable_id=variable_id, + variable_id=variable_id_str, ) resetted = draft_var_srv.reset_variable(draft_workflow, variable) diff --git a/api/controllers/console/app/workflow_run.py b/api/controllers/console/app/workflow_run.py index 2d48b59de2..9b46aeacb8 100644 --- a/api/controllers/console/app/workflow_run.py +++ b/api/controllers/console/app/workflow_run.py @@ -189,7 +189,7 @@ class WorkflowRunExportApi(Resource): @login_required @account_initialization_required @get_app_model() - def get(self, app_model: App, run_id: str): + def get(self, app_model: App, run_id: UUID): tenant_id = str(app_model.tenant_id) app_id = str(app_model.id) run_id_str = str(run_id) diff --git a/api/controllers/console/datasets/datasets_document.py b/api/controllers/console/datasets/datasets_document.py index d387834e9b..bbfbe00a67 100644 --- a/api/controllers/console/datasets/datasets_document.py +++ b/api/controllers/console/datasets/datasets_document.py @@ -979,7 +979,7 @@ class DocumentDownloadApi(DocumentResource): @login_required @account_initialization_required @cloud_edition_billing_rate_limit_check("knowledge") - def get(self, dataset_id: str, document_id: str) -> dict[str, Any]: + def get(self, dataset_id: UUID, document_id: UUID) -> dict[str, Any]: # Reuse the shared permission/tenant checks implemented in DocumentResource. document = self.get_document(str(dataset_id), str(document_id)) return {"url": DocumentService.get_document_download_url(document)} @@ -996,7 +996,7 @@ class DocumentBatchDownloadZipApi(DocumentResource): @account_initialization_required @cloud_edition_billing_rate_limit_check("knowledge") @console_ns.expect(console_ns.models[DocumentBatchDownloadZipPayload.__name__]) - def post(self, dataset_id: str): + def post(self, dataset_id: UUID): """Stream a ZIP archive containing the requested uploaded documents.""" # Parse and validate request payload. payload = DocumentBatchDownloadZipPayload.model_validate(console_ns.payload or {}) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py index b31d73f27d..4baedf662e 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_draft_variable.py @@ -1,6 +1,7 @@ import logging from collections.abc import Callable from typing import Any, NoReturn +from uuid import UUID from flask import Response, request from flask_restx import Resource, marshal, marshal_with @@ -168,21 +169,22 @@ class RagPipelineVariableApi(Resource): @_api_prerequisite @marshal_with(workflow_draft_variable_model) - def get(self, pipeline: Pipeline, variable_id: str): + def get(self, pipeline: Pipeline, variable_id: UUID): draft_var_srv = WorkflowDraftVariableService( session=db.session(), ) - variable = draft_var_srv.get_variable(variable_id=variable_id) + variable_id_str = str(variable_id) + variable = draft_var_srv.get_variable(variable_id=variable_id_str) if variable is None: - raise NotFoundError(description=f"variable not found, id={variable_id}") + raise NotFoundError(description=f"variable not found, id={variable_id_str}") if variable.app_id != pipeline.id: - raise NotFoundError(description=f"variable not found, id={variable_id}") + raise NotFoundError(description=f"variable not found, id={variable_id_str}") return variable @_api_prerequisite @marshal_with(workflow_draft_variable_model) @console_ns.expect(console_ns.models[WorkflowDraftVariablePatchPayload.__name__]) - def patch(self, pipeline: Pipeline, variable_id: str): + def patch(self, pipeline: Pipeline, variable_id: UUID): # Request payload for file types: # # Local File: @@ -210,11 +212,12 @@ class RagPipelineVariableApi(Resource): payload = WorkflowDraftVariablePatchPayload.model_validate(console_ns.payload or {}) args = payload.model_dump(exclude_none=True) - variable = draft_var_srv.get_variable(variable_id=variable_id) + variable_id_str = str(variable_id) + variable = draft_var_srv.get_variable(variable_id=variable_id_str) if variable is None: - raise NotFoundError(description=f"variable not found, id={variable_id}") + raise NotFoundError(description=f"variable not found, id={variable_id_str}") if variable.app_id != pipeline.id: - raise NotFoundError(description=f"variable not found, id={variable_id}") + raise NotFoundError(description=f"variable not found, id={variable_id_str}") new_name = args.get(self._PATCH_NAME_FIELD, None) raw_value = args.get(self._PATCH_VALUE_FIELD, None) @@ -250,15 +253,16 @@ class RagPipelineVariableApi(Resource): return variable @_api_prerequisite - def delete(self, pipeline: Pipeline, variable_id: str): + def delete(self, pipeline: Pipeline, variable_id: UUID): draft_var_srv = WorkflowDraftVariableService( session=db.session(), ) - variable = draft_var_srv.get_variable(variable_id=variable_id) + variable_id_str = str(variable_id) + variable = draft_var_srv.get_variable(variable_id=variable_id_str) if variable is None: - raise NotFoundError(description=f"variable not found, id={variable_id}") + raise NotFoundError(description=f"variable not found, id={variable_id_str}") if variable.app_id != pipeline.id: - raise NotFoundError(description=f"variable not found, id={variable_id}") + raise NotFoundError(description=f"variable not found, id={variable_id_str}") draft_var_srv.delete_variable(variable) db.session.commit() return Response("", 204) @@ -267,7 +271,7 @@ class RagPipelineVariableApi(Resource): @console_ns.route("/rag/pipelines//workflows/draft/variables//reset") class RagPipelineVariableResetApi(Resource): @_api_prerequisite - def put(self, pipeline: Pipeline, variable_id: str): + def put(self, pipeline: Pipeline, variable_id: UUID): draft_var_srv = WorkflowDraftVariableService( session=db.session(), ) @@ -278,11 +282,12 @@ class RagPipelineVariableResetApi(Resource): raise NotFoundError( f"Draft workflow not found, pipeline_id={pipeline.id}", ) - variable = draft_var_srv.get_variable(variable_id=variable_id) + variable_id_str = str(variable_id) + variable = draft_var_srv.get_variable(variable_id=variable_id_str) if variable is None: - raise NotFoundError(description=f"variable not found, id={variable_id}") + raise NotFoundError(description=f"variable not found, id={variable_id_str}") if variable.app_id != pipeline.id: - raise NotFoundError(description=f"variable not found, id={variable_id}") + raise NotFoundError(description=f"variable not found, id={variable_id_str}") resetted = draft_var_srv.reset_variable(draft_workflow, variable) db.session.commit() diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index a7727513df..5d6a779b5a 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -901,7 +901,7 @@ class RagPipelineWorkflowRunNodeExecutionListApi(Resource): @login_required @account_initialization_required @get_rag_pipeline - def get(self, pipeline: Pipeline, run_id: str): + def get(self, pipeline: Pipeline, run_id: UUID): """ Get workflow run node execution list """ diff --git a/api/controllers/service_api/app/annotation.py b/api/controllers/service_api/app/annotation.py index 9f6b1cf52e..12ab692adb 100644 --- a/api/controllers/service_api/app/annotation.py +++ b/api/controllers/service_api/app/annotation.py @@ -174,11 +174,11 @@ class AnnotationUpdateDeleteApi(Resource): ) @validate_app_token @edit_permission_required - def put(self, app_model: App, annotation_id: str): + def put(self, app_model: App, annotation_id: UUID): """Update an existing annotation.""" payload = AnnotationCreatePayload.model_validate(service_api_ns.payload or {}) update_args: UpdateAnnotationArgs = {"question": payload.question, "answer": payload.answer} - annotation = AppAnnotationService.update_app_annotation_directly(update_args, app_model.id, annotation_id) + annotation = AppAnnotationService.update_app_annotation_directly(update_args, app_model.id, str(annotation_id)) response = Annotation.model_validate(annotation, from_attributes=True) return response.model_dump(mode="json") @@ -195,7 +195,7 @@ class AnnotationUpdateDeleteApi(Resource): ) @validate_app_token @edit_permission_required - def delete(self, app_model: App, annotation_id: str): + def delete(self, app_model: App, annotation_id: UUID): """Delete an annotation.""" - AppAnnotationService.delete_app_annotation(app_model.id, annotation_id) + AppAnnotationService.delete_app_annotation(app_model.id, str(annotation_id)) return "", 204 diff --git a/api/controllers/service_api/app/file_preview.py b/api/controllers/service_api/app/file_preview.py index 5e7847d784..44f765d866 100644 --- a/api/controllers/service_api/app/file_preview.py +++ b/api/controllers/service_api/app/file_preview.py @@ -1,5 +1,6 @@ import logging from urllib.parse import quote +from uuid import UUID from flask import Response, request from flask_restx import Resource @@ -50,20 +51,20 @@ class FilePreviewApi(Resource): } ) @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) - def get(self, app_model: App, end_user: EndUser, file_id: str): + def get(self, app_model: App, end_user: EndUser, file_id: UUID): """ Preview/Download a file that was uploaded via Service API. Provides secure file preview/download functionality. Files can only be accessed if they belong to messages within the requesting app's context. """ - file_id = str(file_id) + file_id_str = str(file_id) # Parse query parameters args = FilePreviewQuery.model_validate(request.args.to_dict()) # Validate file ownership and get file objects - _, upload_file = self._validate_file_ownership(file_id, app_model.id) + _, upload_file = self._validate_file_ownership(file_id_str, app_model.id) # Get file content generator try: diff --git a/api/controllers/service_api/dataset/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/service_api/dataset/rag_pipeline/rag_pipeline_workflow.py index 8bc43bccd5..19b1d008b7 100644 --- a/api/controllers/service_api/dataset/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/service_api/dataset/rag_pipeline/rag_pipeline_workflow.py @@ -1,5 +1,6 @@ from collections.abc import Generator from typing import Any +from uuid import UUID from flask import request from pydantic import BaseModel @@ -64,10 +65,11 @@ class DatasourcePluginsApi(DatasetApiResource): 401: "Unauthorized - invalid API token", } ) - def get(self, tenant_id: str, dataset_id: str): + def get(self, tenant_id: str, dataset_id: UUID): """Resource for getting datasource plugins.""" + dataset_id_str = str(dataset_id) # Verify dataset ownership - stmt = select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id) + stmt = select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str) dataset = db.session.scalar(stmt) if not dataset: raise NotFound("Dataset not found.") @@ -77,7 +79,7 @@ class DatasourcePluginsApi(DatasetApiResource): rag_pipeline_service: RagPipelineService = RagPipelineService() datasource_plugins: list[dict[Any, Any]] = rag_pipeline_service.get_datasource_plugins( - tenant_id=tenant_id, dataset_id=dataset_id, is_published=is_published + tenant_id=tenant_id, dataset_id=dataset_id_str, is_published=is_published ) return datasource_plugins, 200 @@ -109,10 +111,11 @@ class DatasourceNodeRunApi(DatasetApiResource): } ) @service_api_ns.expect(service_api_ns.models[DatasourceNodeRunPayload.__name__]) - def post(self, tenant_id: str, dataset_id: str, node_id: str): + def post(self, tenant_id: str, dataset_id: UUID, node_id: str): """Resource for getting datasource plugins.""" + dataset_id_str = str(dataset_id) # Verify dataset ownership - stmt = select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id) + stmt = select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str) dataset = db.session.scalar(stmt) if not dataset: raise NotFound("Dataset not found.") @@ -120,7 +123,7 @@ class DatasourceNodeRunApi(DatasetApiResource): payload = DatasourceNodeRunPayload.model_validate(service_api_ns.payload or {}) assert isinstance(current_user, Account) rag_pipeline_service: RagPipelineService = RagPipelineService() - pipeline: Pipeline = rag_pipeline_service.get_pipeline(tenant_id=tenant_id, dataset_id=dataset_id) + pipeline: Pipeline = rag_pipeline_service.get_pipeline(tenant_id=tenant_id, dataset_id=dataset_id_str) datasource_node_run_api_entity = DatasourceNodeRunApiEntity.model_validate( { **payload.model_dump(exclude_none=True), @@ -172,10 +175,11 @@ class PipelineRunApi(DatasetApiResource): } ) @service_api_ns.expect(service_api_ns.models[PipelineRunApiEntity.__name__]) - def post(self, tenant_id: str, dataset_id: str): + def post(self, tenant_id: str, dataset_id: UUID): """Resource for running a rag pipeline.""" + dataset_id_str = str(dataset_id) # Verify dataset ownership - stmt = select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id) + stmt = select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str) dataset = db.session.scalar(stmt) if not dataset: raise NotFound("Dataset not found.") @@ -186,7 +190,7 @@ class PipelineRunApi(DatasetApiResource): raise Forbidden() rag_pipeline_service: RagPipelineService = RagPipelineService() - pipeline: Pipeline = rag_pipeline_service.get_pipeline(tenant_id=tenant_id, dataset_id=dataset_id) + pipeline: Pipeline = rag_pipeline_service.get_pipeline(tenant_id=tenant_id, dataset_id=dataset_id_str) try: response: dict[Any, Any] | Generator[str, Any, None] = PipelineGenerateService.generate( pipeline=pipeline, diff --git a/api/controllers/service_api/dataset/segment.py b/api/controllers/service_api/dataset/segment.py index 5992fa7410..34e1710068 100644 --- a/api/controllers/service_api/dataset/segment.py +++ b/api/controllers/service_api/dataset/segment.py @@ -1,4 +1,5 @@ from typing import Any +from uuid import UUID from flask import request from flask_restx import marshal @@ -107,17 +108,19 @@ class SegmentApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") @cloud_edition_billing_rate_limit_check("knowledge", "dataset") - def post(self, tenant_id: str, dataset_id: str, document_id: str): + def post(self, tenant_id: str, dataset_id: UUID, document_id: UUID): _, current_tenant_id = current_account_with_tenant() """Create single segment.""" + dataset_id_str = str(dataset_id) # check dataset dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") + document_id_str = str(document_id) # check document - document = DocumentService.get_document(dataset.id, document_id) + document = DocumentService.get_document(dataset.id, document_id_str) if not document: raise NotFound("Document not found.") if document.indexing_status != "completed": @@ -150,7 +153,10 @@ class SegmentApi(DatasetApiResource): for args_item in payload.segments: SegmentService.segment_create_args_validate(args_item, document) segments = SegmentService.multi_create_segment(payload.segments, document, dataset) - return {"data": _marshal_segments_with_summary(segments, dataset_id), "doc_form": document.doc_form}, 200 + return { + "data": _marshal_segments_with_summary(segments, dataset_id_str), + "doc_form": document.doc_form, + }, 200 else: return {"error": "Segments is required"}, 400 @@ -165,19 +171,21 @@ class SegmentApi(DatasetApiResource): 404: "Dataset or document not found", } ) - def get(self, tenant_id: str, dataset_id: str, document_id: str): + def get(self, tenant_id: str, dataset_id: UUID, document_id: UUID): _, current_tenant_id = current_account_with_tenant() """Get segments.""" # check dataset page = request.args.get("page", default=1, type=int) limit = request.args.get("limit", default=20, type=int) + dataset_id_str = str(dataset_id) dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") + document_id_str = str(document_id) # check document - document = DocumentService.get_document(dataset.id, document_id) + document = DocumentService.get_document(dataset.id, document_id_str) if not document: raise NotFound("Document not found.") # check embedding model setting @@ -205,7 +213,7 @@ class SegmentApi(DatasetApiResource): ) segments, total = SegmentService.get_segments( - document_id=document_id, + document_id=document_id_str, tenant_id=current_tenant_id, status_list=args.status, keyword=args.keyword, @@ -214,7 +222,7 @@ class SegmentApi(DatasetApiResource): ) response = { - "data": _marshal_segments_with_summary(segments, dataset_id), + "data": _marshal_segments_with_summary(segments, dataset_id_str), "doc_form": document.doc_form, "total": total, "has_more": len(segments) == limit, @@ -240,22 +248,25 @@ class DatasetSegmentApi(DatasetApiResource): } ) @cloud_edition_billing_rate_limit_check("knowledge", "dataset") - def delete(self, tenant_id: str, dataset_id: str, document_id: str, segment_id: str): + def delete(self, tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID): _, current_tenant_id = current_account_with_tenant() + dataset_id_str = str(dataset_id) # check dataset dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") # check user's model setting DatasetService.check_dataset_model_setting(dataset) + document_id_str = str(document_id) # check document - document = DocumentService.get_document(dataset_id, document_id) + document = DocumentService.get_document(dataset_id_str, document_id_str) if not document: raise NotFound("Document not found.") + segment_id_str = str(segment_id) # check segment - segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_tenant_id) + segment = SegmentService.get_segment_by_id(segment_id=segment_id_str, tenant_id=current_tenant_id) if not segment: raise NotFound("Segment not found.") SegmentService.delete_segment(segment, document, dataset) @@ -276,18 +287,20 @@ class DatasetSegmentApi(DatasetApiResource): ) @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_rate_limit_check("knowledge", "dataset") - def post(self, tenant_id: str, dataset_id: str, document_id: str, segment_id: str): + def post(self, tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID): _, current_tenant_id = current_account_with_tenant() + dataset_id_str = str(dataset_id) # check dataset dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") # check user's model setting DatasetService.check_dataset_model_setting(dataset) + document_id_str = str(document_id) # check document - document = DocumentService.get_document(dataset_id, document_id) + document = DocumentService.get_document(dataset_id_str, document_id_str) if not document: raise NotFound("Document not found.") if dataset.indexing_technique == IndexTechniqueType.HIGH_QUALITY: @@ -306,15 +319,19 @@ class DatasetSegmentApi(DatasetApiResource): ) except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) - # check segment - segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_tenant_id) + segment_id_str = str(segment_id) + # check segment + segment = SegmentService.get_segment_by_id(segment_id=segment_id_str, tenant_id=current_tenant_id) if not segment: raise NotFound("Segment not found.") payload = SegmentUpdatePayload.model_validate(service_api_ns.payload or {}) updated_segment = SegmentService.update_segment(payload.segment, segment, document, dataset) - return {"data": _marshal_segment_with_summary(updated_segment, dataset_id), "doc_form": document.doc_form}, 200 + return { + "data": _marshal_segment_with_summary(updated_segment, dataset_id_str), + "doc_form": document.doc_form, + }, 200 @service_api_ns.doc("get_segment") @service_api_ns.doc(description="Get a specific segment by ID") @@ -325,26 +342,29 @@ class DatasetSegmentApi(DatasetApiResource): 404: "Dataset, document, or segment not found", } ) - def get(self, tenant_id: str, dataset_id: str, document_id: str, segment_id: str): + def get(self, tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID): _, current_tenant_id = current_account_with_tenant() + dataset_id_str = str(dataset_id) # check dataset dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") # check user's model setting DatasetService.check_dataset_model_setting(dataset) + document_id_str = str(document_id) # check document - document = DocumentService.get_document(dataset_id, document_id) + document = DocumentService.get_document(dataset_id_str, document_id_str) if not document: raise NotFound("Document not found.") + segment_id_str = str(segment_id) # check segment - segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_tenant_id) + segment = SegmentService.get_segment_by_id(segment_id=segment_id_str, tenant_id=current_tenant_id) if not segment: raise NotFound("Segment not found.") - return {"data": _marshal_segment_with_summary(segment, dataset_id), "doc_form": document.doc_form}, 200 + return {"data": _marshal_segment_with_summary(segment, dataset_id_str), "doc_form": document.doc_form}, 200 @service_api_ns.route( @@ -369,23 +389,26 @@ class ChildChunkApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") @cloud_edition_billing_rate_limit_check("knowledge", "dataset") - def post(self, tenant_id: str, dataset_id: str, document_id: str, segment_id: str): + def post(self, tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID): _, current_tenant_id = current_account_with_tenant() """Create child chunk.""" + dataset_id_str = str(dataset_id) # check dataset dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") + document_id_str = str(document_id) # check document - document = DocumentService.get_document(dataset.id, document_id) + document = DocumentService.get_document(dataset.id, document_id_str) if not document: raise NotFound("Document not found.") + segment_id_str = str(segment_id) # check segment - segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_tenant_id) + segment = SegmentService.get_segment_by_id(segment_id=segment_id_str, tenant_id=current_tenant_id) if not segment: raise NotFound("Segment not found.") @@ -429,23 +452,26 @@ class ChildChunkApi(DatasetApiResource): 404: "Dataset, document, or segment not found", } ) - def get(self, tenant_id: str, dataset_id: str, document_id: str, segment_id: str): + def get(self, tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID): _, current_tenant_id = current_account_with_tenant() """Get child chunks.""" + dataset_id_str = str(dataset_id) # check dataset dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") + document_id_str = str(document_id) # check document - document = DocumentService.get_document(dataset.id, document_id) + document = DocumentService.get_document(dataset.id, document_id_str) if not document: raise NotFound("Document not found.") + segment_id_str = str(segment_id) # check segment - segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_tenant_id) + segment = SegmentService.get_segment_by_id(segment_id=segment_id_str, tenant_id=current_tenant_id) if not segment: raise NotFound("Segment not found.") @@ -461,7 +487,9 @@ class ChildChunkApi(DatasetApiResource): limit = min(args.limit, 100) keyword = args.keyword - child_chunks = SegmentService.get_child_chunks(segment_id, document_id, dataset_id, page, limit, keyword) + child_chunks = SegmentService.get_child_chunks( + segment_id_str, document_id_str, dataset_id_str, page, limit, keyword + ) return { "data": marshal(child_chunks.items, child_chunk_fields), @@ -497,32 +525,38 @@ class DatasetChildChunkApi(DatasetApiResource): ) @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") @cloud_edition_billing_rate_limit_check("knowledge", "dataset") - def delete(self, tenant_id: str, dataset_id: str, document_id: str, segment_id: str, child_chunk_id: str): + def delete(self, tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID, child_chunk_id: UUID): _, current_tenant_id = current_account_with_tenant() """Delete child chunk.""" + dataset_id_str = str(dataset_id) # check dataset dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") + document_id_str = str(document_id) # check document - document = DocumentService.get_document(dataset.id, document_id) + document = DocumentService.get_document(dataset.id, document_id_str) if not document: raise NotFound("Document not found.") + segment_id_str = str(segment_id) # check segment - segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_tenant_id) + segment = SegmentService.get_segment_by_id(segment_id=segment_id_str, tenant_id=current_tenant_id) if not segment: raise NotFound("Segment not found.") # validate segment belongs to the specified document - if str(segment.document_id) != str(document_id): + if str(segment.document_id) != str(document_id_str): raise NotFound("Document not found.") + child_chunk_id_str = str(child_chunk_id) # check child chunk - child_chunk = SegmentService.get_child_chunk_by_id(child_chunk_id=child_chunk_id, tenant_id=current_tenant_id) + child_chunk = SegmentService.get_child_chunk_by_id( + child_chunk_id=child_chunk_id_str, tenant_id=current_tenant_id + ) if not child_chunk: raise NotFound("Child chunk not found.") @@ -558,32 +592,38 @@ class DatasetChildChunkApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_knowledge_limit_check("add_segment", "dataset") @cloud_edition_billing_rate_limit_check("knowledge", "dataset") - def patch(self, tenant_id: str, dataset_id: str, document_id: str, segment_id: str, child_chunk_id: str): + def patch(self, tenant_id: str, dataset_id: UUID, document_id: UUID, segment_id: UUID, child_chunk_id: UUID): _, current_tenant_id = current_account_with_tenant() """Update child chunk.""" + dataset_id_str = str(dataset_id) # check dataset dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id_str).limit(1) ) if not dataset: raise NotFound("Dataset not found.") + document_id_str = str(document_id) # get document - document = DocumentService.get_document(dataset_id, document_id) + document = DocumentService.get_document(dataset_id_str, document_id_str) if not document: raise NotFound("Document not found.") + segment_id_str = str(segment_id) # get segment - segment = SegmentService.get_segment_by_id(segment_id=segment_id, tenant_id=current_tenant_id) + segment = SegmentService.get_segment_by_id(segment_id=segment_id_str, tenant_id=current_tenant_id) if not segment: raise NotFound("Segment not found.") # validate segment belongs to the specified document - if str(segment.document_id) != str(document_id): + if str(segment.document_id) != str(document_id_str): raise NotFound("Segment not found.") + child_chunk_id_str = str(child_chunk_id) # get child chunk - child_chunk = SegmentService.get_child_chunk_by_id(child_chunk_id=child_chunk_id, tenant_id=current_tenant_id) + child_chunk = SegmentService.get_child_chunk_by_id( + child_chunk_id=child_chunk_id_str, tenant_id=current_tenant_id + ) if not child_chunk: raise NotFound("Child chunk not found.") From ecfee2f072d09991acecc0ee10e57bb300c4d767 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 25 May 2026 15:55:30 +0800 Subject: [PATCH 4/6] fix: center align slider thumb (#36614) --- packages/dify-ui/src/slider/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dify-ui/src/slider/index.tsx b/packages/dify-ui/src/slider/index.tsx index 23719e5c0d..82702c481e 100644 --- a/packages/dify-ui/src/slider/index.tsx +++ b/packages/dify-ui/src/slider/index.tsx @@ -149,7 +149,7 @@ export function Slider({ step={step} disabled={disabled} name={name} - thumbAlignment="edge-client-only" + thumbAlignment="center" className={cn(sliderRootClassName, className)} > From 9ddd98a2650e7550b3cfffb5dc02a1cd75b8037b Mon Sep 17 00:00:00 2001 From: FFXN <31929997+FFXN@users.noreply.github.com> Date: Mon, 25 May 2026 16:06:33 +0800 Subject: [PATCH 5/6] fix(api): preserve dataset nested null shapes (#36611) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: wangxiaolei --- api/fields/dataset_fields.py | 53 ++++-- api/openapi/markdown/console-swagger.md | 40 ++-- api/openapi/markdown/service-swagger.md | 34 ++-- .../unit_tests/fields/test_dataset_fields.py | 173 ++++++++++++++++++ .../api/console/datasets/types.gen.ts | 40 ++-- .../generated/api/console/datasets/zod.gen.ts | 40 ++-- .../generated/api/service/types.gen.ts | 34 ++-- .../generated/api/service/zod.gen.ts | 34 ++-- 8 files changed, 320 insertions(+), 128 deletions(-) create mode 100644 api/tests/unit_tests/fields/test_dataset_fields.py diff --git a/api/fields/dataset_fields.py b/api/fields/dataset_fields.py index ac2e6d19d7..35a22ea404 100644 --- a/api/fields/dataset_fields.py +++ b/api/fields/dataset_fields.py @@ -1,7 +1,7 @@ from datetime import datetime from flask_restx import fields -from pydantic import field_validator +from pydantic import Field, field_validator from fields.base import ResponseModel from libs.helper import TimestampField, to_timestamp @@ -152,31 +152,41 @@ class DatasetRerankingModelResponse(ResponseModel): class DatasetKeywordSettingResponse(ResponseModel): - keyword_weight: float + keyword_weight: float | None = None class DatasetVectorSettingResponse(ResponseModel): - vector_weight: float - embedding_model_name: str - embedding_provider_name: str + vector_weight: float | None = None + embedding_model_name: str | None = None + embedding_provider_name: str | None = None class DatasetWeightedScoreResponse(ResponseModel): weight_type: str | None = None - keyword_setting: DatasetKeywordSettingResponse | None = None - vector_setting: DatasetVectorSettingResponse | None = None + keyword_setting: DatasetKeywordSettingResponse = Field(default_factory=DatasetKeywordSettingResponse) + vector_setting: DatasetVectorSettingResponse = Field(default_factory=DatasetVectorSettingResponse) + + @field_validator("keyword_setting", "vector_setting", mode="before") + @classmethod + def _expand_null_nested(cls, value: object) -> object: + return {} if value is None else value class DatasetRetrievalModelResponse(ResponseModel): search_method: str reranking_enable: bool reranking_mode: str | None = None - reranking_model: DatasetRerankingModelResponse | None + reranking_model: DatasetRerankingModelResponse = Field(default_factory=DatasetRerankingModelResponse) weights: DatasetWeightedScoreResponse | None = None top_k: int score_threshold_enabled: bool score_threshold: float | None = None + @field_validator("reranking_model", mode="before") + @classmethod + def _expand_null_nested(cls, value: object) -> object: + return {} if value is None else value + class DatasetSummaryIndexSettingResponse(ResponseModel): enable: bool | None = None @@ -192,10 +202,10 @@ class DatasetTagResponse(ResponseModel): class DatasetExternalKnowledgeInfoResponse(ResponseModel): - external_knowledge_id: str - external_knowledge_api_id: str - external_knowledge_api_name: str - external_knowledge_api_endpoint: str + external_knowledge_id: str | None = None + external_knowledge_api_id: str | None = None + external_knowledge_api_name: str | None = None + external_knowledge_api_endpoint: str | None = None class DatasetExternalRetrievalModelResponse(ResponseModel): @@ -211,8 +221,8 @@ class DatasetDocMetadataResponse(ResponseModel): class DatasetIconInfoResponse(ResponseModel): - icon_type: str | None - icon: str | None + icon_type: str | None = None + icon: str | None = None icon_background: str | None = None icon_url: str | None = None @@ -237,17 +247,21 @@ class DatasetDetailResponse(ResponseModel): embedding_model_provider: str | None embedding_available: bool | None = None retrieval_model_dict: DatasetRetrievalModelResponse - summary_index_setting: DatasetSummaryIndexSettingResponse | None + summary_index_setting: DatasetSummaryIndexSettingResponse = Field( + default_factory=DatasetSummaryIndexSettingResponse + ) tags: list[DatasetTagResponse] doc_form: str | None - external_knowledge_info: DatasetExternalKnowledgeInfoResponse | None + external_knowledge_info: DatasetExternalKnowledgeInfoResponse = Field( + default_factory=DatasetExternalKnowledgeInfoResponse + ) external_retrieval_model: DatasetExternalRetrievalModelResponse | None doc_metadata: list[DatasetDocMetadataResponse] built_in_field_enabled: bool pipeline_id: str | None runtime_mode: str | None chunk_structure: str | None - icon_info: DatasetIconInfoResponse | None + icon_info: DatasetIconInfoResponse = Field(default_factory=DatasetIconInfoResponse) is_published: bool total_documents: int total_available_documents: int @@ -258,3 +272,8 @@ class DatasetDetailResponse(ResponseModel): @classmethod def _normalize_timestamp(cls, value: datetime | int | None) -> int | None: return to_timestamp(value) + + @field_validator("summary_index_setting", "external_knowledge_info", "icon_info", mode="before") + @classmethod + def _expand_null_nested(cls, value: object) -> object: + return {} if value is None else value diff --git a/api/openapi/markdown/console-swagger.md b/api/openapi/markdown/console-swagger.md index 0d87fc32d0..30f34e6f24 100644 --- a/api/openapi/markdown/console-swagger.md +++ b/api/openapi/markdown/console-swagger.md @@ -11708,9 +11708,9 @@ Condition detail | embedding_model | string | | Yes | | embedding_model_provider | string | | Yes | | enable_api | boolean | | Yes | -| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | Yes | +| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | No | | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | -| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | Yes | +| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | No | | id | string | | Yes | | indexing_technique | string | | Yes | | is_multimodal | boolean | | Yes | @@ -11721,7 +11721,7 @@ Condition detail | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | | Yes | | runtime_mode | string | | Yes | -| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | Yes | +| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | | total_documents | integer | | Yes | @@ -11748,9 +11748,9 @@ Condition detail | embedding_model | string | | Yes | | embedding_model_provider | string | | Yes | | enable_api | boolean | | Yes | -| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | Yes | +| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | No | | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | -| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | Yes | +| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | No | | id | string | | Yes | | indexing_technique | string | | Yes | | is_multimodal | boolean | | Yes | @@ -11762,7 +11762,7 @@ Condition detail | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | | Yes | | runtime_mode | string | | Yes | -| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | Yes | +| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | | total_documents | integer | | Yes | @@ -11790,10 +11790,10 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| external_knowledge_api_endpoint | string | | Yes | -| external_knowledge_api_id | string | | Yes | -| external_knowledge_api_name | string | | Yes | -| external_knowledge_id | string | | Yes | +| external_knowledge_api_endpoint | string | | No | +| external_knowledge_api_id | string | | No | +| external_knowledge_api_name | string | | No | +| external_knowledge_id | string | | No | #### DatasetExternalRetrievalModelResponse @@ -11816,9 +11816,9 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| icon | string | | Yes | +| icon | string | | No | | icon_background | string | | No | -| icon_type | string | | Yes | +| icon_type | string | | No | | icon_url | string | | No | #### DatasetKeywordSetting @@ -11831,7 +11831,7 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| keyword_weight | number | | Yes | +| keyword_weight | number | | No | #### DatasetListItemResponse @@ -11852,9 +11852,9 @@ Condition detail | embedding_model | string | | Yes | | embedding_model_provider | string | | Yes | | enable_api | boolean | | Yes | -| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | Yes | +| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | No | | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | -| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | Yes | +| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | No | | id | string | | Yes | | indexing_technique | string | | Yes | | is_multimodal | boolean | | Yes | @@ -11866,7 +11866,7 @@ Condition detail | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | | Yes | | runtime_mode | string | | Yes | -| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | Yes | +| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | | total_documents | integer | | Yes | @@ -12014,7 +12014,7 @@ Condition detail | ---- | ---- | ----------- | -------- | | reranking_enable | boolean | | Yes | | reranking_mode | string | | No | -| reranking_model | [DatasetRerankingModelResponse](#datasetrerankingmodelresponse) | | Yes | +| reranking_model | [DatasetRerankingModelResponse](#datasetrerankingmodelresponse) | | No | | score_threshold | number | | No | | score_threshold_enabled | boolean | | Yes | | search_method | string | | Yes | @@ -12069,9 +12069,9 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| embedding_model_name | string | | Yes | -| embedding_provider_name | string | | Yes | -| vector_weight | number | | Yes | +| embedding_model_name | string | | No | +| embedding_provider_name | string | | No | +| vector_weight | number | | No | #### DatasetWeightedScore diff --git a/api/openapi/markdown/service-swagger.md b/api/openapi/markdown/service-swagger.md index b89caf0269..ee801b1b8e 100644 --- a/api/openapi/markdown/service-swagger.md +++ b/api/openapi/markdown/service-swagger.md @@ -2338,9 +2338,9 @@ Condition detail | embedding_model | string | | Yes | | embedding_model_provider | string | | Yes | | enable_api | boolean | | Yes | -| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | Yes | +| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | No | | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | -| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | Yes | +| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | No | | id | string | | Yes | | indexing_technique | string | | Yes | | is_multimodal | boolean | | Yes | @@ -2351,7 +2351,7 @@ Condition detail | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | | Yes | | runtime_mode | string | | Yes | -| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | Yes | +| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | | total_documents | integer | | Yes | @@ -2378,9 +2378,9 @@ Condition detail | embedding_model | string | | Yes | | embedding_model_provider | string | | Yes | | enable_api | boolean | | Yes | -| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | Yes | +| external_knowledge_info | [DatasetExternalKnowledgeInfoResponse](#datasetexternalknowledgeinforesponse) | | No | | external_retrieval_model | [DatasetExternalRetrievalModelResponse](#datasetexternalretrievalmodelresponse) | | Yes | -| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | Yes | +| icon_info | [DatasetIconInfoResponse](#dataseticoninforesponse) | | No | | id | string | | Yes | | indexing_technique | string | | Yes | | is_multimodal | boolean | | Yes | @@ -2392,7 +2392,7 @@ Condition detail | provider | string | | Yes | | retrieval_model_dict | [DatasetRetrievalModelResponse](#datasetretrievalmodelresponse) | | Yes | | runtime_mode | string | | Yes | -| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | Yes | +| summary_index_setting | [DatasetSummaryIndexSettingResponse](#datasetsummaryindexsettingresponse) | | No | | tags | [ [DatasetTagResponse](#datasettagresponse) ] | | Yes | | total_available_documents | integer | | Yes | | total_documents | integer | | Yes | @@ -2412,10 +2412,10 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| external_knowledge_api_endpoint | string | | Yes | -| external_knowledge_api_id | string | | Yes | -| external_knowledge_api_name | string | | Yes | -| external_knowledge_id | string | | Yes | +| external_knowledge_api_endpoint | string | | No | +| external_knowledge_api_id | string | | No | +| external_knowledge_api_name | string | | No | +| external_knowledge_id | string | | No | #### DatasetExternalRetrievalModelResponse @@ -2429,16 +2429,16 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| icon | string | | Yes | +| icon | string | | No | | icon_background | string | | No | -| icon_type | string | | Yes | +| icon_type | string | | No | | icon_url | string | | No | #### DatasetKeywordSettingResponse | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| keyword_weight | number | | Yes | +| keyword_weight | number | | No | #### DatasetListQuery @@ -2522,7 +2522,7 @@ Condition detail | ---- | ---- | ----------- | -------- | | reranking_enable | boolean | | Yes | | reranking_mode | string | | No | -| reranking_model | [DatasetRerankingModelResponse](#datasetrerankingmodelresponse) | | Yes | +| reranking_model | [DatasetRerankingModelResponse](#datasetrerankingmodelresponse) | | No | | score_threshold | number | | No | | score_threshold_enabled | boolean | | Yes | | search_method | string | | Yes | @@ -2566,9 +2566,9 @@ Condition detail | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | -| embedding_model_name | string | | Yes | -| embedding_provider_name | string | | Yes | -| vector_weight | number | | Yes | +| embedding_model_name | string | | No | +| embedding_provider_name | string | | No | +| vector_weight | number | | No | #### DatasetWeightedScoreResponse diff --git a/api/tests/unit_tests/fields/test_dataset_fields.py b/api/tests/unit_tests/fields/test_dataset_fields.py new file mode 100644 index 0000000000..125bcb26cf --- /dev/null +++ b/api/tests/unit_tests/fields/test_dataset_fields.py @@ -0,0 +1,173 @@ +from fields.dataset_fields import DatasetDetailResponse + + +def _dataset_detail_payload(**overrides): + payload = { + "id": "ds-1", + "name": "Dataset", + "description": "desc", + "provider": "vendor", + "permission": "only_me", + "data_source_type": None, + "indexing_technique": "economy", + "app_count": 0, + "document_count": 0, + "word_count": 0, + "created_by": "account-1", + "author_name": None, + "created_at": 1704067200, + "updated_by": None, + "updated_at": 1704067200, + "embedding_model": None, + "embedding_model_provider": None, + "embedding_available": True, + "retrieval_model_dict": { + "search_method": "hybrid_search", + "reranking_enable": True, + "reranking_mode": "weighted_score", + "reranking_model": { + "reranking_provider_name": "provider", + "reranking_model_name": "model", + }, + "weights": { + "weight_type": "customized", + "keyword_setting": {"keyword_weight": 0.3}, + "vector_setting": { + "vector_weight": 0.7, + "embedding_model_name": "embedding", + "embedding_provider_name": "provider", + }, + }, + "top_k": 3, + "score_threshold_enabled": False, + "score_threshold": None, + }, + "summary_index_setting": { + "enable": False, + "model_name": None, + "model_provider_name": None, + "summary_prompt": None, + }, + "tags": [], + "doc_form": None, + "external_knowledge_info": { + "external_knowledge_id": "knowledge-id", + "external_knowledge_api_id": "api-id", + "external_knowledge_api_name": "api", + "external_knowledge_api_endpoint": "https://example.com", + }, + "external_retrieval_model": None, + "doc_metadata": [], + "built_in_field_enabled": False, + "pipeline_id": None, + "runtime_mode": "general", + "chunk_structure": None, + "icon_info": { + "icon_type": "emoji", + "icon": "📙", + "icon_background": None, + "icon_url": None, + }, + "is_published": False, + "total_documents": 0, + "total_available_documents": 0, + "enable_api": False, + "is_multimodal": False, + } + payload.update(overrides) + return payload + + +def _dump_dataset_detail(payload): + return DatasetDetailResponse.model_validate(payload).model_dump(mode="json") + + +def test_dataset_detail_expands_legacy_null_nested_fields(): + response = _dump_dataset_detail( + _dataset_detail_payload( + summary_index_setting=None, + external_knowledge_info=None, + icon_info=None, + ) + ) + + assert response["summary_index_setting"] == { + "enable": None, + "model_name": None, + "model_provider_name": None, + "summary_prompt": None, + } + assert response["external_knowledge_info"] == { + "external_knowledge_id": None, + "external_knowledge_api_id": None, + "external_knowledge_api_name": None, + "external_knowledge_api_endpoint": None, + } + assert response["icon_info"] == { + "icon_type": None, + "icon": None, + "icon_background": None, + "icon_url": None, + } + assert response["external_retrieval_model"] is None + + +def test_dataset_detail_expands_legacy_null_retrieval_nested_fields(): + response = _dump_dataset_detail( + _dataset_detail_payload( + retrieval_model_dict={ + "search_method": "hybrid_search", + "reranking_enable": True, + "reranking_mode": "weighted_score", + "reranking_model": None, + "weights": { + "keyword_setting": None, + "vector_setting": None, + }, + "top_k": 3, + "score_threshold_enabled": False, + "score_threshold": None, + } + ) + ) + + assert response["retrieval_model_dict"]["reranking_model"] == { + "reranking_provider_name": None, + "reranking_model_name": None, + } + assert response["retrieval_model_dict"]["weights"] == { + "weight_type": None, + "keyword_setting": {"keyword_weight": None}, + "vector_setting": { + "vector_weight": None, + "embedding_model_name": None, + "embedding_provider_name": None, + }, + } + + +def test_dataset_detail_expands_missing_weighted_score_nested_fields(): + response = _dump_dataset_detail( + _dataset_detail_payload( + retrieval_model_dict={ + "search_method": "hybrid_search", + "reranking_enable": True, + "reranking_mode": "weighted_score", + "reranking_model": None, + "weights": {}, + "top_k": 3, + "score_threshold_enabled": False, + "score_threshold": None, + } + ) + ) + + assert response["retrieval_model_dict"]["weights"] == { + "weight_type": None, + "keyword_setting": {"keyword_weight": None}, + "vector_setting": { + "vector_weight": None, + "embedding_model_name": None, + "embedding_provider_name": None, + }, + } diff --git a/packages/contracts/generated/api/console/datasets/types.gen.ts b/packages/contracts/generated/api/console/datasets/types.gen.ts index 77c46fea16..6c96c80d4a 100644 --- a/packages/contracts/generated/api/console/datasets/types.gen.ts +++ b/packages/contracts/generated/api/console/datasets/types.gen.ts @@ -38,9 +38,9 @@ export type DatasetDetailResponse = { embedding_model: string | null embedding_model_provider: string | null enable_api: boolean - external_knowledge_info: DatasetExternalKnowledgeInfoResponse + external_knowledge_info?: DatasetExternalKnowledgeInfoResponse external_retrieval_model: DatasetExternalRetrievalModelResponse - icon_info: DatasetIconInfoResponse + icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null is_multimodal: boolean @@ -51,7 +51,7 @@ export type DatasetDetailResponse = { provider: string retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: string | null - summary_index_setting: DatasetSummaryIndexSettingResponse + summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number total_documents: number @@ -231,9 +231,9 @@ export type DatasetDetailWithPartialMembersResponse = { embedding_model: string | null embedding_model_provider: string | null enable_api: boolean - external_knowledge_info: DatasetExternalKnowledgeInfoResponse + external_knowledge_info?: DatasetExternalKnowledgeInfoResponse external_retrieval_model: DatasetExternalRetrievalModelResponse - icon_info: DatasetIconInfoResponse + icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null is_multimodal: boolean @@ -245,7 +245,7 @@ export type DatasetDetailWithPartialMembersResponse = { provider: string retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: string | null - summary_index_setting: DatasetSummaryIndexSettingResponse + summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number total_documents: number @@ -465,9 +465,9 @@ export type DatasetListItemResponse = { embedding_model: string | null embedding_model_provider: string | null enable_api: boolean - external_knowledge_info: DatasetExternalKnowledgeInfoResponse + external_knowledge_info?: DatasetExternalKnowledgeInfoResponse external_retrieval_model: DatasetExternalRetrievalModelResponse - icon_info: DatasetIconInfoResponse + icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null is_multimodal: boolean @@ -479,7 +479,7 @@ export type DatasetListItemResponse = { provider: string retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: string | null - summary_index_setting: DatasetSummaryIndexSettingResponse + summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number total_documents: number @@ -497,10 +497,10 @@ export type DatasetDocMetadataResponse = { } export type DatasetExternalKnowledgeInfoResponse = { - external_knowledge_api_endpoint: string - external_knowledge_api_id: string - external_knowledge_api_name: string - external_knowledge_id: string + external_knowledge_api_endpoint?: string | null + external_knowledge_api_id?: string | null + external_knowledge_api_name?: string | null + external_knowledge_id?: string | null } export type DatasetExternalRetrievalModelResponse = { @@ -510,16 +510,16 @@ export type DatasetExternalRetrievalModelResponse = { } export type DatasetIconInfoResponse = { - icon: string | null + icon?: string | null icon_background?: string | null - icon_type: string | null + icon_type?: string | null icon_url?: string | null } export type DatasetRetrievalModelResponse = { reranking_enable: boolean reranking_mode?: string | null - reranking_model: DatasetRerankingModelResponse + reranking_model?: DatasetRerankingModelResponse score_threshold?: number | null score_threshold_enabled: boolean search_method: string @@ -816,13 +816,13 @@ export type DatasetQueryContentResponse = { } export type DatasetKeywordSettingResponse = { - keyword_weight: number + keyword_weight?: number | null } export type DatasetVectorSettingResponse = { - embedding_model_name: string - embedding_provider_name: string - vector_weight: number + embedding_model_name?: string | null + embedding_provider_name?: string | null + vector_weight?: number | null } export type DatasetKeywordSetting = { diff --git a/packages/contracts/generated/api/console/datasets/zod.gen.ts b/packages/contracts/generated/api/console/datasets/zod.gen.ts index c14c4fe455..f795d17f2f 100644 --- a/packages/contracts/generated/api/console/datasets/zod.gen.ts +++ b/packages/contracts/generated/api/console/datasets/zod.gen.ts @@ -307,10 +307,10 @@ export const zDatasetDocMetadataResponse = z.object({ * DatasetExternalKnowledgeInfoResponse */ export const zDatasetExternalKnowledgeInfoResponse = z.object({ - external_knowledge_api_endpoint: z.string(), - external_knowledge_api_id: z.string(), - external_knowledge_api_name: z.string(), - external_knowledge_id: z.string(), + external_knowledge_api_endpoint: z.string().nullish(), + external_knowledge_api_id: z.string().nullish(), + external_knowledge_api_name: z.string().nullish(), + external_knowledge_id: z.string().nullish(), }) /** @@ -326,9 +326,9 @@ export const zDatasetExternalRetrievalModelResponse = z.object({ * DatasetIconInfoResponse */ export const zDatasetIconInfoResponse = z.object({ - icon: z.string().nullable(), + icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: z.string().nullable(), + icon_type: z.string().nullish(), icon_url: z.string().nullish(), }) @@ -654,16 +654,16 @@ export const zHitTestingFile = z.object({ * DatasetKeywordSettingResponse */ export const zDatasetKeywordSettingResponse = z.object({ - keyword_weight: z.number(), + keyword_weight: z.number().nullish(), }) /** * DatasetVectorSettingResponse */ export const zDatasetVectorSettingResponse = z.object({ - embedding_model_name: z.string(), - embedding_provider_name: z.string(), - vector_weight: z.number(), + embedding_model_name: z.string().nullish(), + embedding_provider_name: z.string().nullish(), + vector_weight: z.number().nullish(), }) /** @@ -681,7 +681,7 @@ export const zDatasetWeightedScoreResponse = z.object({ export const zDatasetRetrievalModelResponse = z.object({ reranking_enable: z.boolean(), reranking_mode: z.string().nullish(), - reranking_model: zDatasetRerankingModelResponse, + reranking_model: zDatasetRerankingModelResponse.optional(), score_threshold: z.number().nullish(), score_threshold_enabled: z.boolean(), search_method: z.string(), @@ -708,9 +708,9 @@ export const zDatasetDetailResponse = z.object({ embedding_model: z.string().nullable(), embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), - external_knowledge_info: zDatasetExternalKnowledgeInfoResponse, + external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), external_retrieval_model: zDatasetExternalRetrievalModelResponse, - icon_info: zDatasetIconInfoResponse, + icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), is_multimodal: z.boolean(), @@ -721,7 +721,7 @@ export const zDatasetDetailResponse = z.object({ provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.string().nullable(), - summary_index_setting: zDatasetSummaryIndexSettingResponse, + summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), total_documents: z.int(), @@ -749,9 +749,9 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ embedding_model: z.string().nullable(), embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), - external_knowledge_info: zDatasetExternalKnowledgeInfoResponse, + external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), external_retrieval_model: zDatasetExternalRetrievalModelResponse, - icon_info: zDatasetIconInfoResponse, + icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), is_multimodal: z.boolean(), @@ -763,7 +763,7 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.string().nullable(), - summary_index_setting: zDatasetSummaryIndexSettingResponse, + summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), total_documents: z.int(), @@ -791,9 +791,9 @@ export const zDatasetListItemResponse = z.object({ embedding_model: z.string().nullable(), embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), - external_knowledge_info: zDatasetExternalKnowledgeInfoResponse, + external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), external_retrieval_model: zDatasetExternalRetrievalModelResponse, - icon_info: zDatasetIconInfoResponse, + icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), is_multimodal: z.boolean(), @@ -805,7 +805,7 @@ export const zDatasetListItemResponse = z.object({ provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.string().nullable(), - summary_index_setting: zDatasetSummaryIndexSettingResponse, + summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), total_documents: z.int(), diff --git a/packages/contracts/generated/api/service/types.gen.ts b/packages/contracts/generated/api/service/types.gen.ts index 46be7acdef..54ce811a95 100644 --- a/packages/contracts/generated/api/service/types.gen.ts +++ b/packages/contracts/generated/api/service/types.gen.ts @@ -183,9 +183,9 @@ export type DatasetDetailResponse = { embedding_model: string | null embedding_model_provider: string | null enable_api: boolean - external_knowledge_info: DatasetExternalKnowledgeInfoResponse + external_knowledge_info?: DatasetExternalKnowledgeInfoResponse external_retrieval_model: DatasetExternalRetrievalModelResponse - icon_info: DatasetIconInfoResponse + icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null is_multimodal: boolean @@ -196,7 +196,7 @@ export type DatasetDetailResponse = { provider: string retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: string | null - summary_index_setting: DatasetSummaryIndexSettingResponse + summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number total_documents: number @@ -221,9 +221,9 @@ export type DatasetDetailWithPartialMembersResponse = { embedding_model: string | null embedding_model_provider: string | null enable_api: boolean - external_knowledge_info: DatasetExternalKnowledgeInfoResponse + external_knowledge_info?: DatasetExternalKnowledgeInfoResponse external_retrieval_model: DatasetExternalRetrievalModelResponse - icon_info: DatasetIconInfoResponse + icon_info?: DatasetIconInfoResponse id: string indexing_technique: string | null is_multimodal: boolean @@ -235,7 +235,7 @@ export type DatasetDetailWithPartialMembersResponse = { provider: string retrieval_model_dict: DatasetRetrievalModelResponse runtime_mode: string | null - summary_index_setting: DatasetSummaryIndexSettingResponse + summary_index_setting?: DatasetSummaryIndexSettingResponse tags: Array total_available_documents: number total_documents: number @@ -251,10 +251,10 @@ export type DatasetDocMetadataResponse = { } export type DatasetExternalKnowledgeInfoResponse = { - external_knowledge_api_endpoint: string - external_knowledge_api_id: string - external_knowledge_api_name: string - external_knowledge_id: string + external_knowledge_api_endpoint?: string | null + external_knowledge_api_id?: string | null + external_knowledge_api_name?: string | null + external_knowledge_id?: string | null } export type DatasetExternalRetrievalModelResponse = { @@ -264,14 +264,14 @@ export type DatasetExternalRetrievalModelResponse = { } export type DatasetIconInfoResponse = { - icon: string | null + icon?: string | null icon_background?: string | null - icon_type: string | null + icon_type?: string | null icon_url?: string | null } export type DatasetKeywordSettingResponse = { - keyword_weight: number + keyword_weight?: number | null } export type DatasetListQuery = { @@ -331,7 +331,7 @@ export type DatasetRerankingModelResponse = { export type DatasetRetrievalModelResponse = { reranking_enable: boolean reranking_mode?: string | null - reranking_model: DatasetRerankingModelResponse + reranking_model?: DatasetRerankingModelResponse score_threshold?: number | null score_threshold_enabled: boolean search_method: string @@ -371,9 +371,9 @@ export type DatasetUpdatePayload = { } export type DatasetVectorSettingResponse = { - embedding_model_name: string - embedding_provider_name: string - vector_weight: number + embedding_model_name?: string | null + embedding_provider_name?: string | null + vector_weight?: number | null } export type DatasetWeightedScoreResponse = { diff --git a/packages/contracts/generated/api/service/zod.gen.ts b/packages/contracts/generated/api/service/zod.gen.ts index 98383ec9cd..22e4b24721 100644 --- a/packages/contracts/generated/api/service/zod.gen.ts +++ b/packages/contracts/generated/api/service/zod.gen.ts @@ -217,10 +217,10 @@ export const zDatasetDocMetadataResponse = z.object({ * DatasetExternalKnowledgeInfoResponse */ export const zDatasetExternalKnowledgeInfoResponse = z.object({ - external_knowledge_api_endpoint: z.string(), - external_knowledge_api_id: z.string(), - external_knowledge_api_name: z.string(), - external_knowledge_id: z.string(), + external_knowledge_api_endpoint: z.string().nullish(), + external_knowledge_api_id: z.string().nullish(), + external_knowledge_api_name: z.string().nullish(), + external_knowledge_id: z.string().nullish(), }) /** @@ -236,9 +236,9 @@ export const zDatasetExternalRetrievalModelResponse = z.object({ * DatasetIconInfoResponse */ export const zDatasetIconInfoResponse = z.object({ - icon: z.string().nullable(), + icon: z.string().nullish(), icon_background: z.string().nullish(), - icon_type: z.string().nullable(), + icon_type: z.string().nullish(), icon_url: z.string().nullish(), }) @@ -246,7 +246,7 @@ export const zDatasetIconInfoResponse = z.object({ * DatasetKeywordSettingResponse */ export const zDatasetKeywordSettingResponse = z.object({ - keyword_weight: z.number(), + keyword_weight: z.number().nullish(), }) /** @@ -345,9 +345,9 @@ export const zDatasetTagResponse = z.object({ * DatasetVectorSettingResponse */ export const zDatasetVectorSettingResponse = z.object({ - embedding_model_name: z.string(), - embedding_provider_name: z.string(), - vector_weight: z.number(), + embedding_model_name: z.string().nullish(), + embedding_provider_name: z.string().nullish(), + vector_weight: z.number().nullish(), }) /** @@ -365,7 +365,7 @@ export const zDatasetWeightedScoreResponse = z.object({ export const zDatasetRetrievalModelResponse = z.object({ reranking_enable: z.boolean(), reranking_mode: z.string().nullish(), - reranking_model: zDatasetRerankingModelResponse, + reranking_model: zDatasetRerankingModelResponse.optional(), score_threshold: z.number().nullish(), score_threshold_enabled: z.boolean(), search_method: z.string(), @@ -392,9 +392,9 @@ export const zDatasetDetailResponse = z.object({ embedding_model: z.string().nullable(), embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), - external_knowledge_info: zDatasetExternalKnowledgeInfoResponse, + external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), external_retrieval_model: zDatasetExternalRetrievalModelResponse, - icon_info: zDatasetIconInfoResponse, + icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), is_multimodal: z.boolean(), @@ -405,7 +405,7 @@ export const zDatasetDetailResponse = z.object({ provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.string().nullable(), - summary_index_setting: zDatasetSummaryIndexSettingResponse, + summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), total_documents: z.int(), @@ -433,9 +433,9 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ embedding_model: z.string().nullable(), embedding_model_provider: z.string().nullable(), enable_api: z.boolean(), - external_knowledge_info: zDatasetExternalKnowledgeInfoResponse, + external_knowledge_info: zDatasetExternalKnowledgeInfoResponse.optional(), external_retrieval_model: zDatasetExternalRetrievalModelResponse, - icon_info: zDatasetIconInfoResponse, + icon_info: zDatasetIconInfoResponse.optional(), id: z.string(), indexing_technique: z.string().nullable(), is_multimodal: z.boolean(), @@ -447,7 +447,7 @@ export const zDatasetDetailWithPartialMembersResponse = z.object({ provider: z.string(), retrieval_model_dict: zDatasetRetrievalModelResponse, runtime_mode: z.string().nullable(), - summary_index_setting: zDatasetSummaryIndexSettingResponse, + summary_index_setting: zDatasetSummaryIndexSettingResponse.optional(), tags: z.array(zDatasetTagResponse), total_available_documents: z.int(), total_documents: z.int(), From 23539c5bccc3855668cd6fc956c0268a84e67a51 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 25 May 2026 16:31:52 +0800 Subject: [PATCH 6/6] feat(dify-ui): add status and progress primitives (#36615) --- eslint-suppressions.json | 12 +- packages/dify-ui/package.json | 8 + .../src/progress/__tests__/index.spec.tsx | 80 +++++++++ .../dify-ui/src/progress/index.stories.tsx | 77 ++++++++ packages/dify-ui/src/progress/index.tsx | 167 ++++++++++++++++++ .../src/status-dot/__tests__/index.spec.tsx | 57 ++++++ .../dify-ui/src/status-dot/index.stories.tsx | 62 +++++++ packages/dify-ui/src/status-dot/index.tsx | 108 +++++++++++ .../tools/tool-provider-detail-flow.test.tsx | 6 +- .../[appId]/overview/tracing/config-popup.tsx | 4 +- .../[appId]/overview/tracing/panel.tsx | 4 +- .../config/agent/agent-tools/index.tsx | 4 +- web/app/components/app/log/list.tsx | 10 +- web/app/components/app/overview/app-card.tsx | 4 +- .../components/app/overview/trigger-card.tsx | 21 +-- web/app/components/app/workflow-log/list.tsx | 14 +- .../file-uploader-in-attachment/file-item.tsx | 7 +- .../file-image-item.tsx | 10 +- .../file-uploader-in-chat-input/file-item.tsx | 6 +- .../__tests__/progress-circle.spec.tsx | 89 ---------- .../progress-bar/progress-circle.stories.tsx | 90 ---------- .../base/progress-bar/progress-circle.tsx | 64 ------- .../image-uploader-in-chunk/image-item.tsx | 12 +- .../image-item.tsx | 12 +- .../status-item/__tests__/hooks.spec.ts | 42 +++-- .../status-item/__tests__/index.spec.tsx | 16 +- .../datasets/documents/status-item/hooks.ts | 19 +- .../datasets/documents/status-item/index.tsx | 24 +-- .../datasets/extra-info/api-access/card.tsx | 6 +- .../datasets/extra-info/api-access/index.tsx | 6 +- .../datasets/extra-info/service-api/card.tsx | 8 +- .../datasets/extra-info/service-api/index.tsx | 8 +- .../account-dropdown/__tests__/index.spec.tsx | 6 +- .../header/account-dropdown/index.tsx | 4 +- .../data-source-page-new/item.tsx | 4 +- .../account-setting/key-validator/Operate.tsx | 6 +- .../__tests__/config-model.spec.tsx | 8 +- .../__tests__/credential-selector.spec.tsx | 4 +- ...itch-credential-in-load-balancing.spec.tsx | 16 +- .../__tests__/credential-item.spec.tsx | 4 +- .../model-auth/authorized/credential-item.tsx | 4 +- .../model-auth/config-model.tsx | 6 +- .../model-auth/credential-selector.tsx | 4 +- .../switch-credential-in-load-balancing.tsx | 11 +- .../configuration-button.tsx | 5 +- .../model-selector/popup-item.tsx | 5 +- .../__tests__/credential-panel.spec.tsx | 10 +- .../provider-added-card/credential-panel.tsx | 10 +- .../model-load-balancing-configs.tsx | 4 +- .../header/indicator/__tests__/index.spec.tsx | 79 --------- web/app/components/header/indicator/index.tsx | 54 ------ .../plugins-nav/__tests__/index.spec.tsx | 11 +- .../components/header/plugins-nav/index.tsx | 6 +- .../authorized-in-data-source-node.spec.tsx | 6 +- .../authorized-in-data-source-node.tsx | 6 +- .../plugin-auth/authorized-in-node.tsx | 15 +- .../authorized/__tests__/index.spec.tsx | 4 +- .../plugins/plugin-auth/authorized/index.tsx | 4 +- .../plugins/plugin-auth/authorized/item.tsx | 6 +- .../plugin-auth/plugin-auth-in-agent.tsx | 13 +- .../__tests__/endpoint-card.spec.tsx | 8 +- .../datasource-action-list.tsx | 4 +- .../plugin-detail-panel/endpoint-card.tsx | 6 +- .../tool-selector/components/tool-item.tsx | 6 +- .../__tests__/task-status-indicator.spec.tsx | 12 +- .../components/task-status-indicator.tsx | 13 +- .../components/tools/mcp/detail/content.tsx | 4 +- .../components/tools/mcp/mcp-service-card.tsx | 4 +- .../components/tools/mcp/provider-card.tsx | 6 +- .../tools/provider/__tests__/detail.spec.tsx | 4 +- web/app/components/tools/provider/detail.tsx | 4 +- .../tools/workflow-tool/configure-button.tsx | 4 +- .../nodes/_base/components/setting-item.tsx | 9 +- .../agent/__tests__/integration.spec.tsx | 14 +- .../components/__tests__/model-bar.spec.tsx | 10 +- .../components/__tests__/tool-icon.spec.tsx | 8 +- .../nodes/agent/components/model-bar.tsx | 6 +- .../nodes/agent/components/tool-icon.tsx | 6 +- .../delivery-method/method-item.tsx | 4 +- web/app/components/workflow/run/status.tsx | 16 +- 80 files changed, 841 insertions(+), 679 deletions(-) create mode 100644 packages/dify-ui/src/progress/__tests__/index.spec.tsx create mode 100644 packages/dify-ui/src/progress/index.stories.tsx create mode 100644 packages/dify-ui/src/progress/index.tsx create mode 100644 packages/dify-ui/src/status-dot/__tests__/index.spec.tsx create mode 100644 packages/dify-ui/src/status-dot/index.stories.tsx create mode 100644 packages/dify-ui/src/status-dot/index.tsx delete mode 100644 web/app/components/base/progress-bar/__tests__/progress-circle.spec.tsx delete mode 100644 web/app/components/base/progress-bar/progress-circle.stories.tsx delete mode 100644 web/app/components/base/progress-bar/progress-circle.tsx delete mode 100644 web/app/components/header/indicator/__tests__/index.spec.tsx delete mode 100644 web/app/components/header/indicator/index.tsx diff --git a/eslint-suppressions.json b/eslint-suppressions.json index f1fe2cd252..2624746723 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2534,7 +2534,7 @@ }, "web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx": { "ts/no-explicit-any": { - "count": 3 + "count": 2 } }, "web/app/components/header/account-setting/model-provider-page/model-modal/Input.tsx": { @@ -2653,11 +2653,6 @@ "count": 1 } }, - "web/app/components/plugins/plugin-auth/authorized-in-node.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/plugins/plugin-auth/authorized/index.tsx": { "no-restricted-imports": { "count": 1 @@ -2684,11 +2679,6 @@ "count": 3 } }, - "web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/plugins/plugin-auth/types.ts": { "erasable-syntax-only/enums": { "count": 2 diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json index d98543960c..c517153bf6 100644 --- a/packages/dify-ui/package.json +++ b/packages/dify-ui/package.json @@ -73,6 +73,10 @@ "types": "./src/meter/index.tsx", "import": "./src/meter/index.tsx" }, + "./progress": { + "types": "./src/progress/index.tsx", + "import": "./src/progress/index.tsx" + }, "./number-field": { "types": "./src/number-field/index.tsx", "import": "./src/number-field/index.tsx" @@ -105,6 +109,10 @@ "types": "./src/select/index.tsx", "import": "./src/select/index.tsx" }, + "./status-dot": { + "types": "./src/status-dot/index.tsx", + "import": "./src/status-dot/index.tsx" + }, "./slider": { "types": "./src/slider/index.tsx", "import": "./src/slider/index.tsx" diff --git a/packages/dify-ui/src/progress/__tests__/index.spec.tsx b/packages/dify-ui/src/progress/__tests__/index.spec.tsx new file mode 100644 index 0000000000..07a052d50b --- /dev/null +++ b/packages/dify-ui/src/progress/__tests__/index.spec.tsx @@ -0,0 +1,80 @@ +import { render } from 'vitest-browser-react' +import { ProgressCircle } from '../index' + +describe('ProgressCircle', () => { + it('exposes progressbar semantics through Base UI Progress', async () => { + const screen = await render() + + const progress = screen.getByLabelText('Uploading') + + await expect.element(progress).toHaveAttribute('role', 'progressbar') + await expect.element(progress).toHaveAttribute('aria-valuemin', '0') + await expect.element(progress).toHaveAttribute('aria-valuemax', '100') + await expect.element(progress).toHaveAttribute('aria-valuenow', '40') + }) + + it('supports custom min and max', async () => { + const screen = await render() + + const progress = screen.getByLabelText('Installing') + + await expect.element(progress).toHaveAttribute('aria-valuemin', '1') + await expect.element(progress).toHaveAttribute('aria-valuemax', '5') + await expect.element(progress).toHaveAttribute('aria-valuenow', '3') + }) + + it('renders indeterminate state when value is null', async () => { + const screen = await render() + + await expect.element(screen.getByTestId('progress')).toHaveAttribute('data-indeterminate') + await expect.element(screen.getByTestId('progress')).not.toHaveAttribute('aria-valuenow') + expect(screen.getByTestId('progress').element().querySelector('path')).toBeNull() + }) + + it('does not render a progress sector for zero progress', async () => { + const screen = await render() + + expect(screen.getByTestId('progress').element().querySelector('path')).toBeNull() + }) + + it('applies design kit size variants', async () => { + const screen = await render() + + const root = screen.getByTestId('progress').element() as HTMLElement + const svg = root.querySelector('svg')! + + expect(root.className).toContain('size-5') + expect(svg.getAttribute('width')).toBe('21') + expect(svg.getAttribute('height')).toBe('21') + }) + + it('applies color tokens to circle and sector', async () => { + const screen = await render() + + const root = screen.getByTestId('progress').element() as HTMLElement + const circle = root.querySelector('circle')! + const path = root.querySelector('path')! + + expect(circle.getAttribute('class')).toContain('fill-components-progress-error-bg') + expect(circle.getAttribute('class')).toContain('stroke-components-progress-error-border') + expect(path.getAttribute('class')).toContain('fill-components-progress-error-progress') + }) + + it('renders a deterministic progress sector', async () => { + const screen = await render() + + const path = screen.getByTestId('progress').element().querySelector('path')! + + expect(path.getAttribute('d')).toContain('A 6,6 0 1 1') + }) + + it('renders a closed circle sector for complete progress', async () => { + const screen = await render() + + const path = screen.getByTestId('progress').element().querySelector('path')! + const pathData = path.getAttribute('d')! + + expect(pathData).toContain('A 6,6 0 1 1 6,12') + expect(pathData).toContain('A 6,6 0 1 1 6,0') + }) +}) diff --git a/packages/dify-ui/src/progress/index.stories.tsx b/packages/dify-ui/src/progress/index.stories.tsx new file mode 100644 index 0000000000..eb9a3326ba --- /dev/null +++ b/packages/dify-ui/src/progress/index.stories.tsx @@ -0,0 +1,77 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import type { ProgressCircleColor, ProgressCircleSize } from '.' +import { Fragment } from 'react' +import { ProgressCircle } from '.' + +const colors: ProgressCircleColor[] = ['gray', 'white', 'blue', 'warning', 'error'] +const sizes: ProgressCircleSize[] = ['small', 'medium', 'large'] + +const meta = { + title: 'Base/UI/Progress', + component: ProgressCircle, + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Task progress primitives. ProgressCircle matches the Dify Design Kit circular Progress component and uses Base UI Progress semantics.', + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Circle: Story = { + args: { + 'value': 42, + 'color': 'blue', + 'size': 'small', + 'aria-label': 'Uploading', + }, +} + +export const CircleMatrix: Story = { + args: { + 'value': 62, + 'aria-label': 'Progress', + }, + render: () => ( +
+
+ {sizes.map(size => ( +
+ {size} +
+ ))} + {colors.map(color => ( + +
+ {color} +
+ {sizes.map(size => ( + + ))} +
+ ))} +
+ ), +} + +export const Indeterminate: Story = { + args: { + 'value': null, + 'color': 'gray', + 'size': 'medium', + 'aria-label': 'Processing', + }, +} diff --git a/packages/dify-ui/src/progress/index.tsx b/packages/dify-ui/src/progress/index.tsx new file mode 100644 index 0000000000..05bfd8c355 --- /dev/null +++ b/packages/dify-ui/src/progress/index.tsx @@ -0,0 +1,167 @@ +'use client' + +import type { VariantProps } from 'class-variance-authority' +import { Progress as BaseProgress } from '@base-ui/react/progress' +import { cva } from 'class-variance-authority' +import { cn } from '../cn' + +const progressCircleRootVariants = cva( + 'inline-flex shrink-0 items-center justify-center', + { + variants: { + size: { + small: 'size-3', + medium: 'size-4', + large: 'size-5', + }, + }, + defaultVariants: { + size: 'small', + }, + }, +) + +const progressCircleColorClasses = { + gray: { + stroke: 'stroke-components-progress-gray-border', + fill: 'fill-components-progress-gray-bg', + sector: 'fill-components-progress-gray-progress', + }, + white: { + stroke: 'stroke-components-progress-white-border', + fill: 'fill-components-progress-white-bg', + sector: 'fill-components-progress-white-progress', + }, + blue: { + stroke: 'stroke-components-progress-brand-border', + fill: 'fill-components-progress-brand-bg', + sector: 'fill-components-progress-brand-progress', + }, + warning: { + stroke: 'stroke-components-progress-warning-border', + fill: 'fill-components-progress-warning-bg', + sector: 'fill-components-progress-warning-progress', + }, + error: { + stroke: 'stroke-components-progress-error-border', + fill: 'fill-components-progress-error-bg', + sector: 'fill-components-progress-error-progress', + }, +} as const + +export type ProgressCircleSize = NonNullable['size']> +export type ProgressCircleColor = keyof typeof progressCircleColorClasses + +const progressCircleSizeValues = { + small: 12, + medium: 16, + large: 20, +} as const satisfies Record + +type ProgressCircleAccessibleNameProps + = | { + 'aria-label': string + 'aria-labelledby'?: never + } + | { + 'aria-label'?: never + 'aria-labelledby': string + } + +export type ProgressCircleProps + = Omit + & ProgressCircleAccessibleNameProps + & { + className?: string + color?: ProgressCircleColor + size?: ProgressCircleSize + circleStrokeWidth?: number + } + +function getProgressPercentage(value: number | null, min: number, max: number) { + if (value === null || !Number.isFinite(value) || max <= min) + return null + + return Math.min(100, Math.max(0, ((value - min) / (max - min)) * 100)) +} + +function getSectorPath(size: number, percentage: number | null) { + if (percentage === null || percentage <= 0) + return '' + + const radius = size / 2 + const center = size / 2 + + if (percentage >= 100) { + return ` + M ${center},${center - radius} + A ${radius},${radius} 0 1 1 ${center},${center + radius} + A ${radius},${radius} 0 1 1 ${center},${center - radius} + Z + ` + } + + const angle = (percentage / 100) * 360 + const radians = (angle * Math.PI) / 180 + const x = center + radius * Math.cos(radians - Math.PI / 2) + const y = center + radius * Math.sin(radians - Math.PI / 2) + const largeArcFlag = percentage > 50 ? 1 : 0 + + return ` + M ${center},${center} + L ${center},${center - radius} + A ${radius},${radius} 0 ${largeArcFlag} 1 ${x},${y} + Z + ` +} + +export function ProgressCircle({ + className, + color = 'blue', + size = 'small', + circleStrokeWidth = 1, + value, + min = 0, + max = 100, + ...props +}: ProgressCircleProps) { + const numericSize = progressCircleSizeValues[size] + const percentage = getProgressPercentage(value, min, max) + const radius = numericSize / 2 + const center = numericSize / 2 + const pathData = getSectorPath(numericSize, percentage) + const colors = progressCircleColorClasses[color] + + return ( + + + + ) +} diff --git a/packages/dify-ui/src/status-dot/__tests__/index.spec.tsx b/packages/dify-ui/src/status-dot/__tests__/index.spec.tsx new file mode 100644 index 0000000000..c4b46d50a0 --- /dev/null +++ b/packages/dify-ui/src/status-dot/__tests__/index.spec.tsx @@ -0,0 +1,57 @@ +import { render } from 'vitest-browser-react' +import { StatusDot, StatusDotSkeleton } from '../index' + +describe('StatusDot', () => { + it('renders a medium success dot by default', async () => { + const screen = await render() + + const root = screen.getByTestId('dot').element() as HTMLElement + + await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-hidden', 'true') + expect(root.className).toContain('size-2') + expect(root.className).toContain('bg-components-badge-status-light-success-bg') + expect(root.className).toContain('border-components-badge-status-light-success-border-inner') + expect(root.className).toContain('shadow-status-indicator-green-shadow') + }) + + it('uses small dot geometry', async () => { + const screen = await render() + + const root = screen.getByTestId('dot').element() as HTMLElement + + expect(root.className).toContain('size-1.5') + expect(root.className).toContain('rounded-xs') + }) + + it.each([ + ['warning', 'bg-components-badge-status-light-warning-bg', 'border-components-badge-status-light-warning-border-inner'], + ['error', 'bg-components-badge-status-light-error-bg', 'border-components-badge-status-light-error-border-inner'], + ['normal', 'bg-components-badge-status-light-normal-bg', 'border-components-badge-status-light-normal-border-inner'], + ['disabled', 'bg-components-badge-status-light-disabled-bg', 'border-components-badge-status-light-disabled-border-inner'], + ] as const)('applies %s status tokens', async (status, backgroundClass, borderClass) => { + const screen = await render() + + const dot = screen.getByTestId('dot').element() as HTMLElement + + expect(dot.className).toContain(backgroundClass) + expect(dot.className).toContain(borderClass) + }) + + it('keeps an explicit accessible label visible to assistive tech', async () => { + const screen = await render() + + await expect.element(screen.getByTestId('dot')).toHaveAttribute('aria-label', 'Active') + await expect.element(screen.getByTestId('dot')).not.toHaveAttribute('aria-hidden') + }) + + it('renders skeleton styling without status color', async () => { + const screen = await render() + + const dot = screen.getByTestId('dot').element() as HTMLElement + + expect(dot.className).toContain('bg-text-primary') + expect(dot.className).toContain('opacity-30') + expect(dot.className).not.toContain('bg-components-badge-status-light-success-bg') + expect(dot.className).not.toContain('border-components-badge-status-light-success-border-inner') + }) +}) diff --git a/packages/dify-ui/src/status-dot/index.stories.tsx b/packages/dify-ui/src/status-dot/index.stories.tsx new file mode 100644 index 0000000000..b44deb9682 --- /dev/null +++ b/packages/dify-ui/src/status-dot/index.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import type { StatusDotSize, StatusDotStatus } from '.' +import { Fragment } from 'react' +import { StatusDot, StatusDotSkeleton } from '.' + +const statuses: StatusDotStatus[] = ['success', 'warning', 'error', 'normal', 'disabled'] +const sizes: StatusDotSize[] = ['small', 'medium'] + +const meta = { + title: 'Base/UI/StatusDot', + component: StatusDot, + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Status Dot primitive from the Dify Design Kit. Use it for compact visual status indicators; provide an accessible label only when the dot is the sole status representation.', + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + status: 'success', + size: 'medium', + }, +} + +export const Matrix: Story = { + render: () => ( +
+
+
Small
+
Medium
+ {statuses.map(status => ( + +
+ {status} +
+ {sizes.map(size => ( + + ))} +
+ ))} +
+ ), +} + +export const Skeleton: Story = { + render: () => ( +
+ + +
+ ), +} diff --git a/packages/dify-ui/src/status-dot/index.tsx b/packages/dify-ui/src/status-dot/index.tsx new file mode 100644 index 0000000000..087f3da4ba --- /dev/null +++ b/packages/dify-ui/src/status-dot/index.tsx @@ -0,0 +1,108 @@ +'use client' + +import type { VariantProps } from 'class-variance-authority' +import type { ComponentProps } from 'react' +import { cva } from 'class-variance-authority' +import { cn } from '../cn' + +const statusDotVariants = cva( + 'block shrink-0 border border-solid', + { + variants: { + status: { + success: 'border-components-badge-status-light-success-border-inner bg-components-badge-status-light-success-bg shadow-status-indicator-green-shadow', + warning: 'border-components-badge-status-light-warning-border-inner bg-components-badge-status-light-warning-bg shadow-status-indicator-warning-shadow', + error: 'border-components-badge-status-light-error-border-inner bg-components-badge-status-light-error-bg shadow-status-indicator-red-shadow', + normal: 'border-components-badge-status-light-normal-border-inner bg-components-badge-status-light-normal-bg shadow-status-indicator-blue-shadow', + disabled: 'border-components-badge-status-light-disabled-border-inner bg-components-badge-status-light-disabled-bg shadow-status-indicator-gray-shadow', + }, + size: { + small: 'size-1.5 rounded-xs', + medium: 'size-2 rounded-[3px]', + }, + }, + defaultVariants: { + status: 'success', + size: 'medium', + }, + }, +) + +const statusDotSkeletonVariants = cva( + 'block shrink-0 border border-transparent bg-text-primary opacity-30', + { + variants: { + size: { + small: 'size-1.5 rounded-xs', + medium: 'size-2 rounded-[3px]', + }, + }, + defaultVariants: { + size: 'medium', + }, + }, +) + +type StatusDotVariants = VariantProps + +export type StatusDotStatus = NonNullable +export type StatusDotSize = NonNullable + +export type StatusDotProps + = Omit, 'children'> + & { + status?: StatusDotStatus + size?: StatusDotSize + } + +export type StatusDotSkeletonProps + = Omit, 'children'> + & { + size?: StatusDotSize + } + +export function StatusDot({ + className, + status = 'success', + size = 'medium', + 'aria-hidden': ariaHidden, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + ...props +}: StatusDotProps) { + const hidden = ariaHidden ?? (ariaLabel || ariaLabelledBy ? undefined : true) + + return ( + + ) +} + +export function StatusDotSkeleton({ + className, + size = 'medium', + 'aria-hidden': ariaHidden, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + ...props +}: StatusDotSkeletonProps) { + const hidden = ariaHidden ?? (ariaLabel || ariaLabelledBy ? undefined : true) + + return ( + + ) +} diff --git a/web/__tests__/tools/tool-provider-detail-flow.test.tsx b/web/__tests__/tools/tool-provider-detail-flow.test.tsx index 5e3e94d4ff..7b873601d4 100644 --- a/web/__tests__/tools/tool-provider-detail-flow.test.tsx +++ b/web/__tests__/tools/tool-provider-detail-flow.test.tsx @@ -143,10 +143,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/declaration ConfigurationMethodEnum: { predefinedModel: 'predefined-model' }, })) -vi.mock('@/app/components/header/indicator', () => ({ - default: ({ color }: { color: string }) => , -})) - vi.mock('@/app/components/plugins/card/base/card-icon', () => ({ default: ({ src }: { src: string }) =>
, })) @@ -282,7 +278,7 @@ describe('Tool Provider Detail Flow Integration', () => { await waitFor(() => { expect(screen.getByText('Authorized')).toBeInTheDocument() - expect(screen.getByTestId('indicator-green')).toBeInTheDocument() + expect(document.querySelector('.shadow-status-indicator-green-shadow')).toBeInTheDocument() }) }) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx index 2b0f978906..fa2a176b10 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx @@ -2,6 +2,7 @@ import type { FC, JSX } from 'react' import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' @@ -9,7 +10,6 @@ import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' -import Indicator from '@/app/components/header/indicator' import ProviderConfigModal from './provider-config-modal' import ProviderPanel from './provider-panel' import TracingIcon from './tracing-icon' @@ -330,7 +330,7 @@ const ConfigPopup: FC = ({
{t(`${I18N_PREFIX}.tracing`, { ns: 'app' })}
- +
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 2443641b8a..b03fd6e30d 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type' import type { TracingStatus } from '@/models/app' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { toast } from '@langgenius/dify-ui/toast' import { RiArrowDownDoubleLine, @@ -15,7 +16,6 @@ import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import { AliyunIcon, ArizeIcon, DatabricksIcon, LangfuseIcon, LangsmithIcon, MlflowIcon, OpikIcon, PhoenixIcon, TencentIcon, WeaveIcon } from '@/app/components/base/icons/src/public/tracing' import Loading from '@/app/components/base/loading' -import Indicator from '@/app/components/header/indicator' import { useAppContext } from '@/context/app-context' import { usePathname } from '@/next/navigation' import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps' @@ -290,7 +290,7 @@ const Panel: FC = () => { )} >
- +
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index cb86c8825a..f918ddd7e7 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -7,6 +7,7 @@ import type { AgentTool } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { @@ -24,7 +25,6 @@ import AppIcon from '@/app/components/base/app-icon' import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import { Infotip } from '@/app/components/base/infotip' -import Indicator from '@/app/components/header/indicator' import { CollectionType } from '@/app/components/tools/types' import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' @@ -340,7 +340,7 @@ const AgentTools: FC = () => { }} > {t('notAuthorized', { ns: 'tools' })} - + )}
diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 609a52efc3..e28f5473f6 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -17,6 +17,7 @@ import { DrawerPortal, DrawerViewport, } from '@langgenius/dify-ui/drawer' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiEditFill } from '@remixicon/react' @@ -47,7 +48,6 @@ import { AppSourceType } from '@/service/share' import { useChatConversationDetail, useCompletionConversationDetail } from '@/service/use-log' import { AppModeEnum } from '@/types/app' import PromptLogModal from '../../base/prompt-log-modal' -import Indicator from '../../header/indicator' import { applyAnnotationAdded, applyAnnotationEdited, @@ -114,7 +114,7 @@ const statusTdRender = (statusCount: StatusCount) => { if (statusCount.paused > 0) { return (
- + Pending
) @@ -122,7 +122,7 @@ const statusTdRender = (statusCount: StatusCount) => { else if (statusCount.partial_success + statusCount.failed === 0) { return (
- + Success
) @@ -130,7 +130,7 @@ const statusTdRender = (statusCount: StatusCount) => { else if (statusCount.failed === 0) { return (
- + Partial Success
) @@ -138,7 +138,7 @@ const statusTdRender = (statusCount: StatusCount) => { else { return (
- + {statusCount.failed} {' '} diff --git a/web/app/components/app/overview/app-card.tsx b/web/app/components/app/overview/app-card.tsx index 9b1fc3a032..3bff802c91 100644 --- a/web/app/components/app/overview/app-card.tsx +++ b/web/app/components/app/overview/app-card.tsx @@ -4,6 +4,7 @@ import type { ConfigParams } from './settings' import type { AppDetailResponse } from '@/models/app' import type { AppSSO } from '@/types/app' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { useSuspenseQuery } from '@tanstack/react-query' import * as React from 'react' @@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next' import AppBasic from '@/app/components/app-sidebar/basic' import { useStore as useAppStore } from '@/app/components/app/store' import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button' -import Indicator from '@/app/components/header/indicator' import { useAppContext } from '@/context/app-context' import { useDocLink } from '@/context/i18n' import { AccessMode } from '@/models/access-control' @@ -296,7 +296,7 @@ function AppCard({ } />
- +
{cardState.runningStatus ? t('overview.status.running', { ns: 'appOverview' }) diff --git a/web/app/components/app/overview/trigger-card.tsx b/web/app/components/app/overview/trigger-card.tsx index 05d97312f6..d6bcb9efcf 100644 --- a/web/app/components/app/overview/trigger-card.tsx +++ b/web/app/components/app/overview/trigger-card.tsx @@ -3,6 +3,7 @@ import type { AppDetailResponse } from '@/models/app' import type { AppTrigger } from '@/service/use-tools' import type { AppSSO } from '@/types/app' import type { I18nKeysByPrefix } from '@/types/i18n' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import * as React from 'react' import { useTranslation } from 'react-i18next' @@ -30,20 +31,6 @@ type ITriggerCardProps = { const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => { const { trigger_type, status, provider_name } = trigger - // Status dot styling based on trigger status - const getStatusDot = () => { - if (status === 'enabled') { - return ( -
- ) - } - else { - return ( -
- ) - } - } - // Get BlockEnum type from trigger_type let blockType: BlockEnum switch (trigger_type) { @@ -78,7 +65,11 @@ const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => { size="md" toolIcon={triggerIcon} /> - {getStatusDot()} +
) } diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx index 7b3dae3ab0..402c383a39 100644 --- a/web/app/components/app/workflow-log/list.tsx +++ b/web/app/components/app/workflow-log/list.tsx @@ -12,11 +12,11 @@ import { DrawerPortal, DrawerViewport, } from '@langgenius/dify-ui/drawer' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' -import Indicator from '@/app/components/header/indicator' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' import { AppModeEnum } from '@/types/app' @@ -67,7 +67,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { if (status === 'succeeded') { return (
- + Success
) @@ -75,7 +75,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { if (status === 'failed') { return (
- + Failure
) @@ -83,7 +83,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { if (status === 'stopped') { return (
- + Stop
) @@ -91,7 +91,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { if (status === 'paused') { return (
- + Pending
) @@ -99,7 +99,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { if (status === 'running') { return (
- + Running
) @@ -107,7 +107,7 @@ const WorkflowAppLogList: FC = ({ logs, appDetail, onRefresh }) => { if (status === 'partial-succeeded') { return (
- + Partial Success
) diff --git a/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx index b8521e8799..93ffe2500e 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx @@ -1,5 +1,6 @@ import type { FileEntity } from '../types' import { cn } from '@langgenius/dify-ui/cn' +import { ProgressCircle } from '@langgenius/dify-ui/progress' import { RiDeleteBinLine, RiDownloadLine, @@ -9,11 +10,11 @@ import { memo, useState, } from 'react' +import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { PreviewMode } from '@/app/components/base/features/types' import { ReplayLine } from '@/app/components/base/icons/src/vender/other' import ImagePreview from '@/app/components/base/image-uploader/image-preview' -import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { downloadUrl } from '@/utils/download' import { formatFileSize } from '@/utils/format' @@ -43,6 +44,7 @@ const FileInAttachmentItem = ({ canPreview, previewMode = PreviewMode.CurrentPage, }: FileInAttachmentItemProps) => { + const { t } = useTranslation() const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file const ext = getFileExtension(name, type, isRemote) const isImageFile = supportFileType === SupportUploadFileTypes.image @@ -108,7 +110,8 @@ const FileInAttachmentItem = ({ progress >= 0 && !fileIsUploaded(file) && ( ) } diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx index dbf721b941..8c3b2f8366 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-image-item.tsx @@ -1,5 +1,6 @@ import type { FileEntity } from '../types' import { Button } from '@langgenius/dify-ui/button' +import { ProgressCircle } from '@langgenius/dify-ui/progress' import { RiCloseLine, RiDownloadLine, @@ -8,7 +9,6 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { ReplayLine } from '@/app/components/base/icons/src/vender/other' import ImagePreview from '@/app/components/base/image-uploader/image-preview' -import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' import { downloadUrl } from '@/utils/download' import FileImageRender from '../file-image-render' import { @@ -65,11 +65,9 @@ const FileImageItem = ({ progress >= 0 && !fileIsUploaded(file) && (
) diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx index e8599f1a0b..f4fd96db5b 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx @@ -1,13 +1,13 @@ import type { FileEntity } from '../types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { ProgressCircle } from '@langgenius/dify-ui/progress' import { useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import AudioPreview from '@/app/components/base/file-uploader/audio-preview' import PdfPreview from '@/app/components/base/file-uploader/dynamic-pdf-preview' import VideoPreview from '@/app/components/base/file-uploader/video-preview' -import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' import { downloadUrl } from '@/utils/download' import { formatFileSize } from '@/utils/format' import FileTypeIcon from '../file-type-icon' @@ -110,9 +110,9 @@ const FileItem = ({ { progress >= 0 && !fileIsUploaded(file) && ( ) } diff --git a/web/app/components/base/progress-bar/__tests__/progress-circle.spec.tsx b/web/app/components/base/progress-bar/__tests__/progress-circle.spec.tsx deleted file mode 100644 index 14bb9896e3..0000000000 --- a/web/app/components/base/progress-bar/__tests__/progress-circle.spec.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { render } from '@testing-library/react' -import ProgressCircle from '../progress-circle' - -const extractLargeArcFlag = (pathData: string): string => { - const afterA = pathData.slice(pathData.indexOf('A') + 1) - const tokens = afterA.replace(/,/g, ' ').trim().split(/\s+/) - // Arc syntax: A rx ry x-axis-rotation large-arc-flag sweep-flag x y - return tokens[3]! -} - -describe('ProgressCircle', () => { - describe('Render', () => { - it('renders an SVG with default props', () => { - const { container } = render() - - const svg = container.querySelector('svg') - const circle = container.querySelector('circle') - const path = container.querySelector('path') - - expect(svg)!.toBeInTheDocument() - expect(circle)!.toBeInTheDocument() - expect(path)!.toBeInTheDocument() - }) - }) - - describe('Props', () => { - it('applies correct size and viewBox when size is provided', () => { - const size = 24 - const strokeWidth = 2 - - const { container } = render( - , - ) - - const svg = container.querySelector('svg') as SVGElement - - expect(svg)!.toHaveAttribute('width', String(size + strokeWidth)) - expect(svg)!.toHaveAttribute('height', String(size + strokeWidth)) - expect(svg)!.toHaveAttribute( - 'viewBox', - `0 0 ${size + strokeWidth} ${size + strokeWidth}`, - ) - }) - - it('applies custom stroke and fill classes to the circle', () => { - const { container } = render( - , - ) - const circle = container.querySelector('circle')! - expect(circle!)!.toHaveClass('stroke-red-500') - expect(circle!)!.toHaveClass('fill-red-100') - }) - - it('applies custom sector fill color to the path', () => { - const { container } = render( - , - ) - const path = container.querySelector('path')! - expect(path!)!.toHaveClass('fill-blue-500') - }) - - it('uses large arc flag when percentage is greater than 50', () => { - const { container } = render() - const path = container.querySelector('path')! - const d = path.getAttribute('d') || '' - expect(d).toContain('A') - expect(extractLargeArcFlag(d)).toBe('1') - }) - - it('uses small arc flag when percentage is 50 or less', () => { - const { container } = render() - const path = container.querySelector('path')! - const d = path.getAttribute('d') || '' - expect(d).toContain('A') - expect(extractLargeArcFlag(d)).toBe('0') - }) - - it('uses small arc flag when percentage is exactly 50', () => { - const { container } = render() - const path = container.querySelector('path')! - const d = path.getAttribute('d') || '' - expect(d).toContain('A') - expect(extractLargeArcFlag(d)).toBe('0') - }) - }) -}) diff --git a/web/app/components/base/progress-bar/progress-circle.stories.tsx b/web/app/components/base/progress-bar/progress-circle.stories.tsx deleted file mode 100644 index 2c0e70b4a1..0000000000 --- a/web/app/components/base/progress-bar/progress-circle.stories.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/nextjs-vite' -import { useState } from 'react' -import ProgressCircle from './progress-circle' - -const ProgressCircleDemo = ({ - initialPercentage = 42, - size = 24, -}: { - initialPercentage?: number - size?: number -}) => { - const [percentage, setPercentage] = useState(initialPercentage) - - return ( -
-
- Upload progress - - {percentage} - % - -
-
- - setPercentage(Number.parseInt(event.target.value, 10))} - className="h-2 w-full cursor-pointer appearance-none rounded-full bg-divider-subtle accent-primary-600" - /> -
-
- -
-
- ProgressCircle renders a deterministic SVG slice. Advance the slider to preview how the arc grows for upload indicators. -
-
- ) -} - -const meta = { - title: 'Base/Feedback/ProgressCircle', - component: ProgressCircleDemo, - parameters: { - layout: 'centered', - docs: { - description: { - component: 'Compact radial progress indicator wired to upload flows. The story provides a slider to scrub through percentages.', - }, - }, - }, - argTypes: { - initialPercentage: { - control: { type: 'range', min: 0, max: 100, step: 1 }, - }, - size: { - control: { type: 'number', min: 12, max: 48, step: 2 }, - }, - }, - args: { - initialPercentage: 42, - size: 24, - }, - tags: ['autodocs'], -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Playground: Story = {} - -export const NearComplete: Story = { - args: { - initialPercentage: 92, - }, -} diff --git a/web/app/components/base/progress-bar/progress-circle.tsx b/web/app/components/base/progress-bar/progress-circle.tsx deleted file mode 100644 index 85113277c8..0000000000 --- a/web/app/components/base/progress-bar/progress-circle.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { cn } from '@langgenius/dify-ui/cn' -import { memo } from 'react' - -type ProgressCircleProps = { - className?: string - percentage?: number - size?: number - circleStrokeWidth?: number - circleStrokeColor?: string - circleFillColor?: string - sectorFillColor?: string -} - -const ProgressCircle: React.FC = ({ - className, - percentage = 0, - size = 12, - circleStrokeWidth = 1, - circleStrokeColor = 'stroke-components-progress-brand-border', - circleFillColor = 'fill-components-progress-brand-bg', - sectorFillColor = 'fill-components-progress-brand-progress', -}) => { - const radius = size / 2 - const center = size / 2 - const angle = (percentage / 101) * 360 - const radians = (angle * Math.PI) / 180 - const x = center + radius * Math.cos(radians - Math.PI / 2) - const y = center + radius * Math.sin(radians - Math.PI / 2) - const largeArcFlag = percentage > 50 ? 1 : 0 - - const pathData = ` - M ${center},${center} - L ${center},${center - radius} - A ${radius},${radius} 0 ${largeArcFlag} 1 ${x},${y} - Z - ` - - return ( - - - - - ) -} - -export default memo(ProgressCircle) diff --git a/web/app/components/datasets/common/image-uploader/image-uploader-in-chunk/image-item.tsx b/web/app/components/datasets/common/image-uploader/image-uploader-in-chunk/image-item.tsx index b5a744daac..67ec37e739 100644 --- a/web/app/components/datasets/common/image-uploader/image-uploader-in-chunk/image-item.tsx +++ b/web/app/components/datasets/common/image-uploader/image-uploader-in-chunk/image-item.tsx @@ -1,5 +1,6 @@ import type { FileEntity } from '../types' import { Button } from '@langgenius/dify-ui/button' +import { ProgressCircle } from '@langgenius/dify-ui/progress' import { RiCloseLine, } from '@remixicon/react' @@ -7,9 +8,9 @@ import { memo, useCallback, } from 'react' +import { useTranslation } from 'react-i18next' import FileImageRender from '@/app/components/base/file-uploader/file-image-render' import { ReplayLine } from '@/app/components/base/icons/src/vender/other' -import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' import { fileIsUploaded } from '../utils' type ImageItemProps = { @@ -26,6 +27,7 @@ const ImageItem = ({ onReUpload, onPreview, }: ImageItemProps) => { + const { t } = useTranslation() const { id, progress, base64Url, sourceUrl } = file const handlePreview = useCallback((e: React.MouseEvent) => { @@ -69,11 +71,9 @@ const ImageItem = ({ progress >= 0 && !fileIsUploaded(file) && (
) diff --git a/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/image-item.tsx b/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/image-item.tsx index 1e9ad455ad..35026260c9 100644 --- a/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/image-item.tsx +++ b/web/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing/image-item.tsx @@ -1,5 +1,6 @@ import type { FileEntity } from '../types' import { Button } from '@langgenius/dify-ui/button' +import { ProgressCircle } from '@langgenius/dify-ui/progress' import { RiCloseLine, } from '@remixicon/react' @@ -7,9 +8,9 @@ import { memo, useCallback, } from 'react' +import { useTranslation } from 'react-i18next' import FileImageRender from '@/app/components/base/file-uploader/file-image-render' import { ReplayLine } from '@/app/components/base/icons/src/vender/other' -import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' import { fileIsUploaded } from '../utils' type ImageItemProps = { @@ -26,6 +27,7 @@ const ImageItem = ({ onReUpload, onPreview, }: ImageItemProps) => { + const { t } = useTranslation() const { id, progress, base64Url, sourceUrl } = file const handlePreview = useCallback((e: React.MouseEvent) => { @@ -69,11 +71,9 @@ const ImageItem = ({ progress >= 0 && !fileIsUploaded(file) && (
) diff --git a/web/app/components/datasets/documents/status-item/__tests__/hooks.spec.ts b/web/app/components/datasets/documents/status-item/__tests__/hooks.spec.ts index 9b89cab7a0..6d1a0d6881 100644 --- a/web/app/components/datasets/documents/status-item/__tests__/hooks.spec.ts +++ b/web/app/components/datasets/documents/status-item/__tests__/hooks.spec.ts @@ -19,46 +19,45 @@ describe('useIndexStatus', () => { expect(keys).toEqual(expect.arrayContaining(expectedKeys)) }) - // Verify each status entry has the correct color - describe('colors', () => { - it('should return orange color for queuing', () => { + describe('status variants', () => { + it('should return warning status for queuing', () => { const { result } = renderHook(() => useIndexStatus()) - expect(result.current.queuing.color).toBe('orange') + expect(result.current.queuing.status).toBe('warning') }) - it('should return blue color for indexing', () => { + it('should return normal status for indexing', () => { const { result } = renderHook(() => useIndexStatus()) - expect(result.current.indexing.color).toBe('blue') + expect(result.current.indexing.status).toBe('normal') }) - it('should return orange color for paused', () => { + it('should return warning status for paused', () => { const { result } = renderHook(() => useIndexStatus()) - expect(result.current.paused.color).toBe('orange') + expect(result.current.paused.status).toBe('warning') }) - it('should return red color for error', () => { + it('should return error status for error', () => { const { result } = renderHook(() => useIndexStatus()) - expect(result.current.error.color).toBe('red') + expect(result.current.error.status).toBe('error') }) - it('should return green color for available', () => { + it('should return success status for available', () => { const { result } = renderHook(() => useIndexStatus()) - expect(result.current.available.color).toBe('green') + expect(result.current.available.status).toBe('success') }) - it('should return green color for enabled', () => { + it('should return success status for enabled', () => { const { result } = renderHook(() => useIndexStatus()) - expect(result.current.enabled.color).toBe('green') + expect(result.current.enabled.status).toBe('success') }) - it('should return gray color for disabled', () => { + it('should return disabled status for disabled', () => { const { result } = renderHook(() => useIndexStatus()) - expect(result.current.disabled.color).toBe('gray') + expect(result.current.disabled.status).toBe('disabled') }) - it('should return gray color for archived', () => { + it('should return disabled status for archived', () => { const { result } = renderHook(() => useIndexStatus()) - expect(result.current.archived.color).toBe('gray') + expect(result.current.archived.status).toBe('disabled') }) }) @@ -105,14 +104,13 @@ describe('useIndexStatus', () => { }) }) - // Verify each entry has both color and text properties - it('should return objects with color and text properties for every status', () => { + it('should return objects with status and text properties for every status', () => { const { result } = renderHook(() => useIndexStatus()) for (const key of Object.keys(result.current) as Array) { - expect(result.current[key]).toHaveProperty('color') + expect(result.current[key]).toHaveProperty('status') expect(result.current[key]).toHaveProperty('text') - expect(typeof result.current[key].color).toBe('string') + expect(typeof result.current[key].status).toBe('string') expect(typeof result.current[key].text).toBe('string') } }) diff --git a/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx b/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx index 1e651929fe..324e714242 100644 --- a/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx +++ b/web/app/components/datasets/documents/status-item/__tests__/index.spec.tsx @@ -34,14 +34,14 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ // Mock useIndexStatus hook vi.mock('../hooks', () => ({ useIndexStatus: () => ({ - queuing: { text: 'Queuing', color: 'orange' }, - indexing: { text: 'Indexing', color: 'blue' }, - paused: { text: 'Paused', color: 'yellow' }, - error: { text: 'Error', color: 'red' }, - available: { text: 'Available', color: 'green' }, - enabled: { text: 'Enabled', color: 'green' }, - disabled: { text: 'Disabled', color: 'gray' }, - archived: { text: 'Archived', color: 'gray' }, + queuing: { text: 'Queuing', status: 'warning' }, + indexing: { text: 'Indexing', status: 'normal' }, + paused: { text: 'Paused', status: 'warning' }, + error: { text: 'Error', status: 'error' }, + available: { text: 'Available', status: 'success' }, + enabled: { text: 'Enabled', status: 'success' }, + disabled: { text: 'Disabled', status: 'disabled' }, + archived: { text: 'Archived', status: 'disabled' }, }), })) diff --git a/web/app/components/datasets/documents/status-item/hooks.ts b/web/app/components/datasets/documents/status-item/hooks.ts index 270aa36023..69423ff4c4 100644 --- a/web/app/components/datasets/documents/status-item/hooks.ts +++ b/web/app/components/datasets/documents/status-item/hooks.ts @@ -1,15 +1,16 @@ +import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot' import { useTranslation } from 'react-i18next' export const useIndexStatus = () => { const { t } = useTranslation() return { - queuing: { color: 'orange', text: t('list.status.queuing', { ns: 'datasetDocuments' }) }, // waiting - indexing: { color: 'blue', text: t('list.status.indexing', { ns: 'datasetDocuments' }) }, // indexing splitting parsing cleaning - paused: { color: 'orange', text: t('list.status.paused', { ns: 'datasetDocuments' }) }, // paused - error: { color: 'red', text: t('list.status.error', { ns: 'datasetDocuments' }) }, // error - available: { color: 'green', text: t('list.status.available', { ns: 'datasetDocuments' }) }, // completed,archived = false,enabled = true - enabled: { color: 'green', text: t('list.status.enabled', { ns: 'datasetDocuments' }) }, // completed,archived = false,enabled = true - disabled: { color: 'gray', text: t('list.status.disabled', { ns: 'datasetDocuments' }) }, // completed,archived = false,enabled = false - archived: { color: 'gray', text: t('list.status.archived', { ns: 'datasetDocuments' }) }, // completed,archived = true - } + queuing: { status: 'warning', text: t('list.status.queuing', { ns: 'datasetDocuments' }) }, + indexing: { status: 'normal', text: t('list.status.indexing', { ns: 'datasetDocuments' }) }, + paused: { status: 'warning', text: t('list.status.paused', { ns: 'datasetDocuments' }) }, + error: { status: 'error', text: t('list.status.error', { ns: 'datasetDocuments' }) }, + available: { status: 'success', text: t('list.status.available', { ns: 'datasetDocuments' }) }, + enabled: { status: 'success', text: t('list.status.enabled', { ns: 'datasetDocuments' }) }, + disabled: { status: 'disabled', text: t('list.status.disabled', { ns: 'datasetDocuments' }) }, + archived: { status: 'disabled', text: t('list.status.archived', { ns: 'datasetDocuments' }) }, + } satisfies Record } diff --git a/web/app/components/datasets/documents/status-item/index.tsx b/web/app/components/datasets/documents/status-item/index.tsx index 911b1df55e..fb875fee57 100644 --- a/web/app/components/datasets/documents/status-item/index.tsx +++ b/web/app/components/datasets/documents/status-item/index.tsx @@ -1,8 +1,9 @@ +import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot' import type { OperationName } from '../types' -import type { ColorMap, IndicatorProps } from '@/app/components/header/indicator' import type { CommonResponse } from '@/models/common' import type { DocumentDisplayStatus } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' @@ -11,19 +12,17 @@ import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Infotip } from '@/app/components/base/infotip' -import Indicator from '@/app/components/header/indicator' import { useDocumentDelete, useDocumentDisable, useDocumentEnable } from '@/service/knowledge/use-document' import { asyncRunSafe } from '@/utils' import s from '../style.module.css' import { useIndexStatus } from './hooks' -const STATUS_TEXT_COLOR_MAP: ColorMap = { - green: 'text-util-colors-green-green-600', - orange: 'text-util-colors-warning-warning-600', - red: 'text-util-colors-red-red-600', - blue: 'text-util-colors-blue-light-blue-light-600', - yellow: 'text-util-colors-warning-warning-600', - gray: 'text-text-tertiary', +const STATUS_TEXT_COLOR_MAP: Record = { + success: 'text-util-colors-green-green-600', + warning: 'text-util-colors-warning-warning-600', + error: 'text-util-colors-red-red-600', + normal: 'text-util-colors-blue-light-blue-light-600', + disabled: 'text-text-tertiary', } type StatusItemProps = { status: DocumentDisplayStatus @@ -43,6 +42,7 @@ const StatusItem = ({ status, reverse = false, scene = 'list', textCls = '', err const { t } = useTranslation() const DOC_INDEX_STATUS_MAP = useIndexStatus() const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP + const statusItem = DOC_INDEX_STATUS_MAP[localStatus] const { enabled = false, archived = false, id = '' } = detail || {} const { mutateAsync: enableDocument } = useDocumentEnable() const { mutateAsync: disableDocument } = useDocumentDisable() @@ -78,9 +78,9 @@ const StatusItem = ({ status, reverse = false, scene = 'list', textCls = '', err }, [localStatus]) return (
- - - {DOC_INDEX_STATUS_MAP[localStatus]?.text} + + + {statusItem.text} {errorMessage && (
-
{expand &&
{t('appMenus.apiAccess', { ns: 'common' })}
} -
diff --git a/web/app/components/datasets/extra-info/service-api/card.tsx b/web/app/components/datasets/extra-info/service-api/card.tsx index efa5cf7f6a..c178f875fd 100644 --- a/web/app/components/datasets/extra-info/service-api/card.tsx +++ b/web/app/components/datasets/extra-info/service-api/card.tsx @@ -1,11 +1,11 @@ import { Button } from '@langgenius/dify-ui/button' import { PopoverClose } from '@langgenius/dify-ui/popover' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { RiBookOpenLine, RiKey2Line } from '@remixicon/react' import * as React from 'react' import { useTranslation } from 'react-i18next' import CopyFeedback from '@/app/components/base/copy-feedback' import { ApiAggregate } from '@/app/components/base/icons/src/vender/knowledge' -import Indicator from '@/app/components/header/indicator' import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url' import Link from '@/next/link' @@ -35,10 +35,10 @@ const Card = ({
-
-
{t('serviceApi.title', { ns: 'dataset' })}
diff --git a/web/app/components/header/account-dropdown/__tests__/index.spec.tsx b/web/app/components/header/account-dropdown/__tests__/index.spec.tsx index f23cb2efd4..107ebd7028 100644 --- a/web/app/components/header/account-dropdown/__tests__/index.spec.tsx +++ b/web/app/components/header/account-dropdown/__tests__/index.spec.tsx @@ -353,8 +353,7 @@ describe('AccountDropdown', () => { fireEvent.click(screen.getByRole('button')) // Assert - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toHaveClass('bg-components-badge-status-light-warning-bg') + expect(document.querySelector('.bg-components-badge-status-light-warning-bg')).toBeInTheDocument() }) it('should show green indicator when version is latest', () => { @@ -374,8 +373,7 @@ describe('AccountDropdown', () => { fireEvent.click(screen.getByRole('button')) // Assert - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toHaveClass('bg-components-badge-status-light-success-bg') + expect(document.querySelector('.bg-components-badge-status-light-success-bg')).toBeInTheDocument() }) }) }) diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 3744db6e81..a64fc5f5a7 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -3,6 +3,7 @@ import type { MouseEventHandler, ReactNode } from 'react' import { Avatar } from '@langgenius/dify-ui/avatar' import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLinkItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@langgenius/dify-ui/dropdown-menu' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useSuspenseQuery } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -22,7 +23,6 @@ import { systemFeaturesQueryOptions } from '@/service/system-features' import { useLogout } from '@/service/use-common' import AccountAbout from '../account-about' import GithubStar from '../github-star' -import Indicator from '../indicator' import Compliance from './compliance' import { ExternalLinkIndicator, MenuItemContent } from './menu-item-content' import Support from './support' @@ -216,7 +216,7 @@ export default function AppSelector() { trailing={(
{langGeniusVersionInfo.current_version}
- +
)} /> diff --git a/web/app/components/header/account-setting/data-source-page-new/item.tsx b/web/app/components/header/account-setting/data-source-page-new/item.tsx index 8fb3ca305a..167d055f1d 100644 --- a/web/app/components/header/account-setting/data-source-page-new/item.tsx +++ b/web/app/components/header/account-setting/data-source-page-new/item.tsx @@ -2,13 +2,13 @@ import type { DataSourceCredential, } from './types' import { Button } from '@langgenius/dify-ui/button' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { memo, useState, } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' -import Indicator from '@/app/components/header/indicator' import Operator from './operator' type ItemProps = { @@ -76,7 +76,7 @@ const Item = ({ }
- +
connected diff --git a/web/app/components/header/account-setting/key-validator/Operate.tsx b/web/app/components/header/account-setting/key-validator/Operate.tsx index 7f09f32f64..984f572a0d 100644 --- a/web/app/components/header/account-setting/key-validator/Operate.tsx +++ b/web/app/components/header/account-setting/key-validator/Operate.tsx @@ -1,6 +1,6 @@ import type { Status } from './declarations' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useTranslation } from 'react-i18next' -import Indicator from '../../indicator' type OperateProps = { isOpen: boolean @@ -71,13 +71,13 @@ const Operate = ({ status === 'fail' && (
{t('provider.invalidApiKey', { ns: 'common' })}
- +
) } { status === 'success' && ( - + ) }
({ })) // Mock Indicator -vi.mock('@/app/components/header/indicator', () => ({ - default: ({ color }: { color: string }) =>
, +vi.mock('@langgenius/dify-ui/status-dot', () => ({ + StatusDot: ({ status }: { status: string }) =>
, })) describe('ConfigModel', () => { @@ -19,7 +19,7 @@ describe('ConfigModel', () => { expect(screen.getByText(/modelProvider.auth.authorizationError/)).toBeInTheDocument() expect(screen.getByTestId('scales-icon')).toBeInTheDocument() - expect(screen.getByTestId('indicator-orange')).toBeInTheDocument() + expect(screen.getByTestId('indicator-warning')).toBeInTheDocument() fireEvent.click(screen.getByText(/modelProvider.auth.authorizationError/)) expect(onClick).toHaveBeenCalled() @@ -29,7 +29,7 @@ describe('ConfigModel', () => { render() expect(screen.getByText(/modelProvider.auth.credentialRemoved/)).toBeInTheDocument() - expect(screen.getByTestId('indicator-red')).toBeInTheDocument() + expect(screen.getByTestId('indicator-error')).toBeInTheDocument() }) it('should render standard config message when no flags enabled', () => { diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/credential-selector.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/credential-selector.spec.tsx index 720bdc2ff3..408465fbb2 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/credential-selector.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/credential-selector.spec.tsx @@ -10,8 +10,8 @@ vi.mock('../authorized/credential-item', () => ({ ), })) -vi.mock('@/app/components/header/indicator', () => ({ - default: () =>
, +vi.mock('@langgenius/dify-ui/status-dot', () => ({ + StatusDot: () =>
, })) vi.mock('@remixicon/react', () => ({ diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx index 73aa8f9bfc..e135c23764 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/switch-credential-in-load-balancing.spec.tsx @@ -15,8 +15,8 @@ vi.mock('../authorized', () => ({ ), })) -vi.mock('@/app/components/header/indicator', () => ({ - default: ({ color }: { color: string }) =>
, +vi.mock('@langgenius/dify-ui/status-dot', () => ({ + StatusDot: ({ status }: { status: string }) =>
, })) vi.mock('@remixicon/react', () => ({ @@ -58,7 +58,7 @@ describe('SwitchCredentialInLoadBalancing', () => { ) expect(screen.getByText('Key 1'))!.toBeInTheDocument() - expect(screen.getByTestId('indicator-green'))!.toBeInTheDocument() + expect(screen.getByTestId('indicator-success'))!.toBeInTheDocument() }) it('should render auth removed status when selected credential is not in list', () => { @@ -73,7 +73,7 @@ describe('SwitchCredentialInLoadBalancing', () => { ) expect(screen.getByText(/modelProvider.auth.authRemoved/))!.toBeInTheDocument() - expect(screen.getByTestId('indicator-red'))!.toBeInTheDocument() + expect(screen.getByTestId('indicator-error'))!.toBeInTheDocument() }) it('should render unavailable status when credentials list is empty', () => { @@ -156,7 +156,7 @@ describe('SwitchCredentialInLoadBalancing', () => { />, ) - expect(screen.getByTestId('indicator-red'))!.toBeInTheDocument() + expect(screen.getByTestId('indicator-error'))!.toBeInTheDocument() expect(screen.getByText(/auth.credentialUnavailableInButton/))!.toBeInTheDocument() }) @@ -244,9 +244,9 @@ describe('SwitchCredentialInLoadBalancing', () => { />, ) - // indicator-green shown (not authRemoved, not unavailable, not empty) - // indicator-green shown (not authRemoved, not unavailable, not empty) - expect(screen.getByTestId('indicator-green'))!.toBeInTheDocument() + // indicator-success shown (not authRemoved, not unavailable, not empty) + // indicator-success shown (not authRemoved, not unavailable, not empty) + expect(screen.getByTestId('indicator-success'))!.toBeInTheDocument() // credential_name is empty so nothing printed for name // credential_name is empty so nothing printed for name // credential_name is empty so nothing printed for name diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/__tests__/credential-item.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/__tests__/credential-item.spec.tsx index c5ef0028fb..0b96f03c7d 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/__tests__/credential-item.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/__tests__/credential-item.spec.tsx @@ -2,8 +2,8 @@ import type { Credential } from '../../../declarations' import { fireEvent, render, screen } from '@testing-library/react' import CredentialItem from '../credential-item' -vi.mock('@/app/components/header/indicator', () => ({ - default: () =>
, +vi.mock('@langgenius/dify-ui/status-dot', () => ({ + StatusDot: () =>
, })) describe('CredentialItem', () => { diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx index 8e315ccd75..610e94890c 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/credential-item.tsx @@ -1,5 +1,6 @@ import type { Credential } from '../../declarations' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo, @@ -8,7 +9,6 @@ import { import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import Badge from '@/app/components/base/badge' -import Indicator from '@/app/components/header/indicator' type CredentialItemProps = { credential: Credential @@ -71,7 +71,7 @@ const CredentialItem = ({
) } - +
void @@ -30,7 +30,7 @@ const ConfigModel = ({ > {t('modelProvider.auth.authorizationError', { ns: 'common' })} - +
) } @@ -49,7 +49,7 @@ const ConfigModel = ({ credentialRemoved && ( <> {t('modelProvider.auth.credentialRemoved', { ns: 'common' })} - + ) } diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx index 85ff918847..df1287113f 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/credential-selector.tsx @@ -4,6 +4,7 @@ import { PopoverContent, PopoverTrigger, } from '@langgenius/dify-ui/popover' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { RiAddLine, RiArrowDownSLine, @@ -15,7 +16,6 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' -import Indicator from '@/app/components/header/indicator' import CredentialItem from './authorized/credential-item' type CredentialSelectorProps = { @@ -60,7 +60,7 @@ const CredentialSelector = ({ selectedCredential && (
{ - !selectedCredential.addNewCredential && + !selectedCredential.addNewCredential && }
{selectedCredential.credential_name}
{ diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx index 30bc17d159..aaaa8f5dfa 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/switch-credential-in-load-balancing.tsx @@ -1,3 +1,4 @@ +import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot' import type { Dispatch, SetStateAction } from 'react' import type { Credential, @@ -6,6 +7,7 @@ import type { } from '../declarations' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowDownSLine } from '@remixicon/react' import { @@ -15,7 +17,6 @@ import { import { useTranslation } from 'react-i18next' import Badge from '@/app/components/base/badge' import { ConfigurationMethodEnum, ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import Indicator from '@/app/components/header/indicator' import Authorized from './authorized' type SwitchCredentialInLoadBalancingProps = { @@ -49,9 +50,9 @@ const SwitchCredentialInLoadBalancing = ({ const authRemoved = selectedCredentialId && !currentCredential && !empty const unavailable = currentCredential?.not_allowed_to_use - let color = 'green' + let color: StatusDotStatus = 'success' if (authRemoved || unavailable) - color = 'red' + color = 'error' const Item = (
-
+
) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index 0a936d1aa5..24a91f5599 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -4,6 +4,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { ComboboxGroup, ComboboxItem, ComboboxItemIndicator } from '@langgenius/dify-ui/combobox' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' import { PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' @@ -122,13 +123,13 @@ function PopupItem({ : credentialName ? ( <> - + {credentialName} ) : ( <> - + {t('modelProvider.selector.configureRequired', { ns: 'common' })} )} diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx index 9d158a019e..257b0128ab 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/__tests__/credential-panel.spec.tsx @@ -85,8 +85,8 @@ vi.mock('../model-auth-dropdown', () => ({ ), })) -vi.mock('@/app/components/header/indicator', () => ({ - default: ({ color }: { color: string }) =>
, +vi.mock('@langgenius/dify-ui/status-dot', () => ({ + StatusDot: ({ status }: { status: string }) =>
, })) vi.mock('@/app/components/base/icons/src/vender/line/alertsAndFeedback/Warning', () => ({ @@ -192,7 +192,7 @@ describe('CredentialPanel', () => { it('should show green indicator and credential name for api-fallback (exhausted + authorized key)', () => { mockTrialCredits.isExhausted = true renderWithQueryClient(createProvider()) - expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green') + expect(screen.getByTestId('indicator')).toHaveAttribute('data-status', 'success') expect(screen.getByText('test-credential')).toBeInTheDocument() }) @@ -206,7 +206,7 @@ describe('CredentialPanel', () => { renderWithQueryClient(createProvider({ preferred_provider_type: PreferredProviderTypeEnum.custom, })) - expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green') + expect(screen.getByTestId('indicator')).toHaveAttribute('data-status', 'success') expect(screen.getByText('test-credential')).toBeInTheDocument() }) @@ -228,7 +228,7 @@ describe('CredentialPanel', () => { available_credentials: [{ credential_id: 'cred-1', credential_name: 'Bad Key' }], }, })) - expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'red') + expect(screen.getByTestId('indicator')).toHaveAttribute('data-status', 'error') expect(screen.getByText('Bad Key')).toBeInTheDocument() }) }) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx index 190b05209f..25234a582f 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx @@ -1,9 +1,9 @@ import type { ModelProvider } from '../declarations' import type { CardVariant } from './use-credential-panel-state' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { memo } from 'react' import { useTranslation } from 'react-i18next' import Warning from '@/app/components/base/icons/src/vender/line/alertsAndFeedback/Warning' -import Indicator from '@/app/components/header/indicator' import ModelAuthDropdown from './model-auth-dropdown' import SystemQuotaCard from './system-quota-card' import { useChangeProviderPriority } from './use-change-provider-priority' @@ -38,7 +38,7 @@ const CredentialPanel = ({ {isTextLabel ? - : } + : } - + + )} /> diff --git a/web/app/components/header/indicator/__tests__/index.spec.tsx b/web/app/components/header/indicator/__tests__/index.spec.tsx deleted file mode 100644 index ffb2ade8d3..0000000000 --- a/web/app/components/header/indicator/__tests__/index.spec.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { render, screen } from '@testing-library/react' -import Indicator from '../index' - -describe('Indicator', () => { - it('should render with default props', () => { - render() - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toBeInTheDocument() - expect(indicator).toHaveClass( - 'bg-components-badge-status-light-success-bg', - ) - expect(indicator).toHaveClass( - 'border-components-badge-status-light-success-border-inner', - ) - expect(indicator).toHaveClass('shadow-status-indicator-green-shadow') - }) - - it('should render with orange color', () => { - render() - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toHaveClass( - 'bg-components-badge-status-light-warning-bg', - ) - expect(indicator).toHaveClass( - 'border-components-badge-status-light-warning-border-inner', - ) - expect(indicator).toHaveClass('shadow-status-indicator-warning-shadow') - }) - - it('should render with red color', () => { - render() - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toHaveClass('bg-components-badge-status-light-error-bg') - expect(indicator).toHaveClass( - 'border-components-badge-status-light-error-border-inner', - ) - expect(indicator).toHaveClass('shadow-status-indicator-red-shadow') - }) - - it('should render with blue color', () => { - render() - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toHaveClass('bg-components-badge-status-light-normal-bg') - expect(indicator).toHaveClass( - 'border-components-badge-status-light-normal-border-inner', - ) - expect(indicator).toHaveClass('shadow-status-indicator-blue-shadow') - }) - - it('should render with yellow color', () => { - render() - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toHaveClass( - 'bg-components-badge-status-light-warning-bg', - ) - expect(indicator).toHaveClass( - 'border-components-badge-status-light-warning-border-inner', - ) - expect(indicator).toHaveClass('shadow-status-indicator-warning-shadow') - }) - - it('should render with gray color', () => { - render() - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toHaveClass( - 'bg-components-badge-status-light-disabled-bg', - ) - expect(indicator).toHaveClass( - 'border-components-badge-status-light-disabled-border-inner', - ) - expect(indicator).toHaveClass('shadow-status-indicator-gray-shadow') - }) - - it('should apply custom className', () => { - render() - const indicator = screen.getByTestId('status-indicator') - expect(indicator).toHaveClass('custom-class') - }) -}) diff --git a/web/app/components/header/indicator/index.tsx b/web/app/components/header/indicator/index.tsx deleted file mode 100644 index c60ebcb7bd..0000000000 --- a/web/app/components/header/indicator/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client' - -import { cn } from '@langgenius/dify-ui/cn' - -export type IndicatorProps = { - color?: 'green' | 'orange' | 'red' | 'blue' | 'yellow' | 'gray' - className?: string -} - -export type ColorMap = { - green: string - orange: string - red: string - blue: string - yellow: string - gray: string -} - -const BACKGROUND_MAP: ColorMap = { - green: 'bg-components-badge-status-light-success-bg', - orange: 'bg-components-badge-status-light-warning-bg', - red: 'bg-components-badge-status-light-error-bg', - blue: 'bg-components-badge-status-light-normal-bg', - yellow: 'bg-components-badge-status-light-warning-bg', - gray: 'bg-components-badge-status-light-disabled-bg', -} -const BORDER_MAP: ColorMap = { - green: 'border-components-badge-status-light-success-border-inner', - orange: 'border-components-badge-status-light-warning-border-inner', - red: 'border-components-badge-status-light-error-border-inner', - blue: 'border-components-badge-status-light-normal-border-inner', - yellow: 'border-components-badge-status-light-warning-border-inner', - gray: 'border-components-badge-status-light-disabled-border-inner', -} -const SHADOW_MAP: ColorMap = { - green: 'shadow-status-indicator-green-shadow', - orange: 'shadow-status-indicator-warning-shadow', - red: 'shadow-status-indicator-red-shadow', - blue: 'shadow-status-indicator-blue-shadow', - yellow: 'shadow-status-indicator-warning-shadow', - gray: 'shadow-status-indicator-gray-shadow', -} - -export default function Indicator({ - color = 'green', - className = '', -}: IndicatorProps) { - return ( -
- ) -} diff --git a/web/app/components/header/plugins-nav/__tests__/index.spec.tsx b/web/app/components/header/plugins-nav/__tests__/index.spec.tsx index ab55225641..ff32260811 100644 --- a/web/app/components/header/plugins-nav/__tests__/index.spec.tsx +++ b/web/app/components/header/plugins-nav/__tests__/index.spec.tsx @@ -5,6 +5,9 @@ import { useSelectedLayoutSegment } from '@/next/navigation' import PluginsNav from '../index' +const queryErrorStatusDot = (container: HTMLElement) => + container.querySelector('.shadow-status-indicator-red-shadow') + vi.mock('@/next/navigation', () => ({ useSelectedLayoutSegment: vi.fn(), })) @@ -38,7 +41,7 @@ describe('PluginsNav', () => { const svg = linkElement.querySelector('svg') expect(svg).toBeInTheDocument() - expect(screen.queryByTestId('status-indicator')).not.toBeInTheDocument() + expect(queryErrorStatusDot(linkElement)).not.toBeInTheDocument() }) describe('Active State', () => { @@ -70,7 +73,7 @@ describe('PluginsNav', () => { expect(svgs.length).toBe(1) expect(svgs[0]).toHaveClass('install-icon') - expect(screen.queryByTestId('status-indicator')).not.toBeInTheDocument() + expect(queryErrorStatusDot(container)).not.toBeInTheDocument() }) it('renders Installing With Error state (Inactive)', () => { @@ -81,7 +84,7 @@ describe('PluginsNav', () => { const downloadingIcon = container.querySelector('.install-icon') expect(downloadingIcon).toBeInTheDocument() - expect(screen.getByTestId('status-indicator')).toBeInTheDocument() + expect(queryErrorStatusDot(container)).toBeInTheDocument() }) it('renders Failed state (Inactive)', () => { @@ -93,7 +96,7 @@ describe('PluginsNav', () => { expect(svg).toBeInTheDocument() expect(svg).not.toHaveClass('install-icon') - expect(screen.getByTestId('status-indicator')).toBeInTheDocument() + expect(queryErrorStatusDot(container)).toBeInTheDocument() }) it('renders Default icon when Active even if installing', () => { diff --git a/web/app/components/header/plugins-nav/index.tsx b/web/app/components/header/plugins-nav/index.tsx index ca3ebdbbb6..8e5ef258b2 100644 --- a/web/app/components/header/plugins-nav/index.tsx +++ b/web/app/components/header/plugins-nav/index.tsx @@ -1,9 +1,9 @@ 'use client' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useTranslation } from 'react-i18next' import { Group } from '@/app/components/base/icons/src/vender/other' -import Indicator from '@/app/components/header/indicator' import { usePluginTaskStatus } from '@/app/components/plugins/plugin-page/plugin-tasks/hooks' import Link from '@/next/link' import { useSelectedLayoutSegment } from '@/next/navigation' @@ -37,8 +37,8 @@ const PluginsNav = ({ > { (isFailed || isInstallingWithError) && !activated && ( - ) diff --git a/web/app/components/plugins/plugin-auth/__tests__/authorized-in-data-source-node.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/authorized-in-data-source-node.spec.tsx index d5cea7a495..76adaf5721 100644 --- a/web/app/components/plugins/plugin-auth/__tests__/authorized-in-data-source-node.spec.tsx +++ b/web/app/components/plugins/plugin-auth/__tests__/authorized-in-data-source-node.spec.tsx @@ -2,8 +2,8 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import AuthorizedInDataSourceNode from '../authorized-in-data-source-node' -vi.mock('@/app/components/header/indicator', () => ({ - default: ({ color }: { color: string }) => , +vi.mock('@langgenius/dify-ui/status-dot', () => ({ + StatusDot: ({ status }: { status: string }) => , })) describe('AuthorizedInDataSourceNode', () => { @@ -19,7 +19,7 @@ describe('AuthorizedInDataSourceNode', () => { it('renders with green indicator', () => { render() - expect(screen.getByTestId('indicator')).toHaveAttribute('data-color', 'green') + expect(screen.getByTestId('indicator')).toHaveAttribute('data-status', 'success') }) it('renders singular text for 1 authorization', () => { diff --git a/web/app/components/plugins/plugin-auth/authorized-in-data-source-node.tsx b/web/app/components/plugins/plugin-auth/authorized-in-data-source-node.tsx index d8b1245c16..c8dda200bc 100644 --- a/web/app/components/plugins/plugin-auth/authorized-in-data-source-node.tsx +++ b/web/app/components/plugins/plugin-auth/authorized-in-data-source-node.tsx @@ -1,11 +1,11 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { RiEqualizer2Line } from '@remixicon/react' import { memo, } from 'react' import { useTranslation } from 'react-i18next' -import Indicator from '@/app/components/header/indicator' type AuthorizedInDataSourceNodeProps = { authorizationsNum: number @@ -22,9 +22,9 @@ const AuthorizedInDataSourceNode = ({ size="small" onClick={onJumpToDataSourcePage} > - { authorizationsNum > 1 diff --git a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx index 12a84b56e3..0831fe9a15 100644 --- a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx +++ b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx @@ -1,9 +1,11 @@ +import type { StatusDotStatus } from '@langgenius/dify-ui/status-dot' import type { Credential, PluginPayload, } from './types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { RiArrowDownSLine } from '@remixicon/react' import { memo, @@ -11,7 +13,6 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import Indicator from '@/app/components/header/indicator' import { Authorized, usePluginAuth, @@ -41,7 +42,7 @@ const AuthorizedInNode = ({ let label = '' let removed = false let unavailable = false - let color = 'green' + let color: StatusDotStatus = 'success' let defaultUnavailable = false if (!credentialId) { label = t('auth.workspaceDefault', { ns: 'plugin' }) @@ -49,7 +50,7 @@ const AuthorizedInNode = ({ const defaultCredential = credentials.find(c => c.is_default) if (defaultCredential?.not_allowed_to_use) { - color = 'gray' + color = 'disabled' defaultUnavailable = true } } @@ -60,9 +61,9 @@ const AuthorizedInNode = ({ unavailable = !!credential?.not_allowed_to_use && !credential?.from_enterprise if (removed) - color = 'red' + color = 'error' else if (unavailable) - color = 'gray' + color = 'disabled' } return (
) } -
- {label} { diff --git a/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-card.spec.tsx b/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-card.spec.tsx index 1dab8bdf84..d2ac595c87 100644 --- a/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-card.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/__tests__/endpoint-card.spec.tsx @@ -72,8 +72,8 @@ vi.mock('@/service/use-endpoints', () => ({ }), })) -vi.mock('@/app/components/header/indicator', () => ({ - default: ({ color }: { color: string }) => , +vi.mock('@langgenius/dify-ui/status-dot', () => ({ + StatusDot: ({ status }: { status: string }) => , })) vi.mock('@/app/components/tools/utils/to-form-schema', () => ({ @@ -176,7 +176,7 @@ describe('EndpointCard', () => { render() expect(screen.getByText('plugin.detailPanel.serviceOk'))!.toBeInTheDocument() - expect(screen.getByTestId('indicator'))!.toHaveAttribute('data-color', 'green') + expect(screen.getByTestId('indicator'))!.toHaveAttribute('data-status', 'success') }) it('should show disabled status when not enabled', () => { @@ -184,7 +184,7 @@ describe('EndpointCard', () => { render() expect(screen.getByText('plugin.detailPanel.disabled'))!.toBeInTheDocument() - expect(screen.getByTestId('indicator'))!.toHaveAttribute('data-color', 'gray') + expect(screen.getByTestId('indicator'))!.toHaveAttribute('data-status', 'disabled') }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx b/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx index 5bba7b823b..b8768b16a1 100644 --- a/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/datasource-action-list.tsx @@ -1,6 +1,6 @@ // import { useAppContext } from '@/context/app-context' // import { Button } from '@langgenius/dify-ui/button' -// import Indicator from '@/app/components/header/indicator' +// import { StatusDot } from '@langgenius/dify-ui/status-dot' // import ToolItem from '@/app/components/tools/provider/tool-item' // import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import type { PluginDetail } from '@/app/components/plugins/types' @@ -60,7 +60,7 @@ const ActionList = ({ onClick={() => setShowSettingAuth(true)} disabled={!isCurrentWorkspaceManager} > - + {t('tools.auth.authorized')} )} */} diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx index 9cea1defbc..8fff7c7da5 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx @@ -8,6 +8,7 @@ import { AlertDialogContent, AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { toast } from '@langgenius/dify-ui/toast' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' @@ -18,7 +19,6 @@ import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files' -import Indicator from '@/app/components/header/indicator' import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { useDeleteEndpoint, @@ -199,13 +199,13 @@ const EndpointCard = ({
{active && (
- + {t('detailPanel.serviceOk', { ns: 'plugin' })}
)} {!active && (
- + {t('detailPanel.disabled', { ns: 'plugin' })}
)} diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx index 2bb690777c..06735865f7 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx @@ -2,6 +2,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { RiDeleteBinLine, @@ -14,7 +15,6 @@ import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' import AppIcon from '@/app/components/base/app-icon' import { Group } from '@/app/components/base/icons/src/vender/other' -import Indicator from '@/app/components/header/indicator' import { InstallPluginButton } from '@/app/components/workflow/nodes/_base/components/install-plugin-button' import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' import McpToolNotSupportTooltip from '@/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip' @@ -128,13 +128,13 @@ const ToolItem = ({ {!isError && !uninstalled && !versionMismatch && noAuth && ( )} {!isError && !uninstalled && !versionMismatch && authRemoved && ( )} {!isError && !uninstalled && versionMismatch && installInfo && ( diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/components/__tests__/task-status-indicator.spec.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/components/__tests__/task-status-indicator.spec.tsx index addae49d98..8311a54f44 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/components/__tests__/task-status-indicator.spec.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/components/__tests__/task-status-indicator.spec.tsx @@ -1,9 +1,9 @@ import { fireEvent, render, screen } from '@testing-library/react' import TaskStatusIndicator from '../task-status-indicator' -vi.mock('@/app/components/base/progress-bar/progress-circle', () => ({ - default: ({ percentage }: { percentage: number }) => ( -
+vi.mock('@langgenius/dify-ui/progress', () => ({ + ProgressCircle: ({ value }: { value: number }) => ( +
), })) @@ -68,7 +68,7 @@ describe('TaskStatusIndicator', () => { />, ) const progress = screen.getByTestId('progress-circle') - expect(progress).toHaveAttribute('data-percentage', '40') + expect(progress).toHaveAttribute('data-value', '40') }) it('should show progress circle when isInstallingWithSuccess', () => { @@ -81,7 +81,7 @@ describe('TaskStatusIndicator', () => { />, ) const progress = screen.getByTestId('progress-circle') - expect(progress).toHaveAttribute('data-percentage', '75') + expect(progress).toHaveAttribute('data-value', '75') }) it('should show error progress circle when isInstallingWithError', () => { @@ -106,7 +106,7 @@ describe('TaskStatusIndicator', () => { />, ) const progress = screen.getByTestId('progress-circle') - expect(progress).toHaveAttribute('data-percentage', '0') + expect(progress).toHaveAttribute('data-value', '0') }) }) diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx index ae75566aae..dfab072746 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/components/task-status-indicator.tsx @@ -1,8 +1,8 @@ import type { FC } from 'react' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { ProgressCircle } from '@langgenius/dify-ui/progress' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' -import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon' type TaskStatusIndicatorProps = { @@ -67,16 +67,15 @@ const TaskStatusIndicator: FC = ({
{(isInstalling || isInstallingWithSuccess) && ( 0 ? successPluginsLength / totalPluginsLength : 0) * 100} - circleFillColor="fill-components-progress-brand-bg" + value={(totalPluginsLength > 0 ? successPluginsLength / totalPluginsLength : 0) * 100} + aria-label={tip} /> )} {isInstallingWithError && ( 0 ? runningPluginsLength / totalPluginsLength : 0) * 100} - circleFillColor="fill-components-progress-brand-bg" - sectorFillColor="fill-components-progress-error-border" - circleStrokeColor="stroke-components-progress-error-border" + value={(totalPluginsLength > 0 ? runningPluginsLength / totalPluginsLength : 0) * 100} + color="error" + aria-label={tip} /> )} {showSuccessIcon && !isInstalling && !isInstallingWithSuccess && !isInstallingWithError && ( diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx index b83fb847b1..e78d88b783 100644 --- a/web/app/components/tools/mcp/detail/content.tsx +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -12,6 +12,7 @@ import { } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useBoolean } from 'ahooks' import copy from 'copy-to-clipboard' @@ -19,7 +20,6 @@ import * as React from 'react' import { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' -import Indicator from '@/app/components/header/indicator' import Icon from '@/app/components/plugins/card/base/card-icon' import { useAppContext } from '@/context/app-context' import { openOAuthPopup } from '@/hooks/use-oauth' @@ -231,7 +231,7 @@ const MCPDetailContent: FC = ({ onClick={handleAuthorize} disabled={!isCurrentWorkspaceManager} > - + {t('auth.authorized', { ns: 'tools' })} )} diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx index 38c757f841..a897d39dbd 100644 --- a/web/app/components/tools/mcp/mcp-service-card.tsx +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -16,6 +16,7 @@ import { import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiEditLine, RiLoopLeftLine } from '@remixicon/react' @@ -24,7 +25,6 @@ import { useTranslation } from 'react-i18next' import CopyFeedback from '@/app/components/base/copy-feedback' import Divider from '@/app/components/base/divider' import { Mcp } from '@/app/components/base/icons/src/vender/other' -import Indicator from '@/app/components/header/indicator' import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { useDocLink } from '@/context/i18n' @@ -40,7 +40,7 @@ const StatusIndicator: FC = ({ serverActivated }) => { const { t } = useTranslation() return (
- +
{serverActivated ? t('overview.status.running', { ns: 'appOverview' }) diff --git a/web/app/components/tools/mcp/provider-card.tsx b/web/app/components/tools/mcp/provider-card.tsx index 45e5a24f68..4536d96cad 100644 --- a/web/app/components/tools/mcp/provider-card.tsx +++ b/web/app/components/tools/mcp/provider-card.tsx @@ -9,11 +9,11 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { RiHammerFill } from '@remixicon/react' import { useBoolean } from 'ahooks' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import Indicator from '@/app/components/header/indicator' import Icon from '@/app/components/plugins/card/base/card-icon' import { useAppContext } from '@/context/app-context' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' @@ -112,11 +112,11 @@ const MCPCard = ({
/
{`${t('mcp.updateTime', { ns: 'tools' })} ${formatTimeFromNow(data.updated_at! * 1000)}`}
- {data.is_team_authorization && data.tools.length > 0 && } + {data.is_team_authorization && data.tools.length > 0 && } {(!data.is_team_authorization || !data.tools.length) && (
{t('mcp.noConfigured', { ns: 'tools' })} - +
)}
diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx index 5a26589e11..70a35f3ddb 100644 --- a/web/app/components/tools/provider/__tests__/detail.spec.tsx +++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx @@ -83,8 +83,8 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ }, })) -vi.mock('@/app/components/header/indicator', () => ({ - default: () => , +vi.mock('@langgenius/dify-ui/status-dot', () => ({ + StatusDot: () => , })) vi.mock('@/app/components/plugins/card/base/card-icon', () => ({ diff --git a/web/app/components/tools/provider/detail.tsx b/web/app/components/tools/provider/detail.tsx index bf878a6206..ff64e0044c 100644 --- a/web/app/components/tools/provider/detail.tsx +++ b/web/app/components/tools/provider/detail.tsx @@ -20,6 +20,7 @@ import { DrawerPortal, DrawerViewport, } from '@langgenius/dify-ui/drawer' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine, @@ -31,7 +32,6 @@ import ActionButton from '@/app/components/base/action-button' import { LinkExternal02, Settings01 } from '@/app/components/base/icons/src/vender/line/general' import Loading from '@/app/components/base/loading' import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import Indicator from '@/app/components/header/indicator' import Icon from '@/app/components/plugins/card/base/card-icon' import Description from '@/app/components/plugins/card/base/description' import OrgInfo from '@/app/components/plugins/card/base/org-info' @@ -325,7 +325,7 @@ const ProviderDetail = ({ }} disabled={!isCurrentWorkspaceManager} > - + {t('auth.authorized', { ns: 'tools' })} )} diff --git a/web/app/components/tools/workflow-tool/configure-button.tsx b/web/app/components/tools/workflow-tool/configure-button.tsx index 5b83e37d3a..0adc923e48 100644 --- a/web/app/components/tools/workflow-tool/configure-button.tsx +++ b/web/app/components/tools/workflow-tool/configure-button.tsx @@ -1,9 +1,9 @@ 'use client' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' -import Indicator from '@/app/components/header/indicator' import { useRouter } from '@/next/navigation' import Divider from '../../base/divider' @@ -90,7 +90,7 @@ const WorkflowToolConfigureButton = ({ disabled={!isCurrentWorkspaceManager || disabled} > {t('common.configure', { ns: 'workflow' })} - {outdated && } + {outdated && }
)} /> @@ -83,7 +83,7 @@ export const ModelBar: FC = (props) => { readonly deprecatedClassName="opacity-50" /> - {showWarn && } + {showWarn && }
) diff --git a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx index f672730523..d898497e42 100644 --- a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx +++ b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx @@ -1,11 +1,11 @@ import type { ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import { Group } from '@/app/components/base/icons/src/vender/other' -import Indicator from '@/app/components/header/indicator' import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' import { getIconFromMarketPlace } from '@/utils/get-icon' @@ -50,7 +50,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { return 'not-authorized' return undefined }, [currentProvider, isDataReady]) - const indicator = status === 'not-installed' ? 'red' : status === 'not-authorized' ? 'yellow' : undefined + const indicator = status === 'not-installed' ? 'error' : status === 'not-authorized' ? 'warning' : undefined const notSuccess = (['not-installed', 'not-authorized'] as Array).includes(status) const { t } = useTranslation() const tooltip = useMemo(() => { @@ -96,7 +96,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
{iconContent}
- {indicator && } + {indicator && }
) diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx index 5a997d0d15..97a086a38b 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/method-item.tsx @@ -6,6 +6,7 @@ import type { } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { @@ -19,7 +20,6 @@ import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' import Badge from '@/app/components/base/badge/index' -import Indicator from '@/app/components/header/indicator' import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { DeliveryMethodType } from '../../types' import EmailConfigureModal from './email-configure-modal' @@ -177,7 +177,7 @@ const DeliveryMethodItem: FC = ({ disabled={readonly} > {t(`${i18nPrefix}.deliveryMethod.notConfigured`, { ns: 'workflow' })} - + )}
diff --git a/web/app/components/workflow/run/status.tsx b/web/app/components/workflow/run/status.tsx index a13b52816d..b9b21df9dd 100644 --- a/web/app/components/workflow/run/status.tsx +++ b/web/app/components/workflow/run/status.tsx @@ -1,9 +1,9 @@ 'use client' import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' +import { StatusDot } from '@langgenius/dify-ui/status-dot' import { useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' -import Indicator from '@/app/components/header/indicator' import StatusContainer from '@/app/components/workflow/run/status-container' import { useDocLink } from '@/context/i18n' import { useWorkflowPausedDetails } from '@/service/use-log' @@ -112,43 +112,43 @@ const StatusPanel: FC = ({ > {status === 'running' && ( <> - + {isListening ? 'Listening' : 'Running'} )} {status === 'succeeded' && ( <> - + SUCCESS )} {status === 'partial-succeeded' && ( <> - + PARTIAL SUCCESS )} {status === 'exception' && ( <> - + EXCEPTION )} {status === 'failed' && ( <> - + FAIL )} {status === 'stopped' && ( <> - + STOP )} {status === 'paused' && ( <> - + PENDING )}