Procházet zdrojové kódy

Merge branch 'main' into feat/rag-2

tags/2.0.0-beta.1
twwu před 2 měsíci
rodič
revize
a1666fe058
100 změnil soubory, kde provedl 5513 přidání a 568 odebrání
  1. 3
    0
      .github/workflows/autofix.yml
  2. 2
    0
      .gitignore
  3. 7
    0
      api/.env.example
  4. 9
    0
      api/configs/feature/__init__.py
  5. 2
    3
      api/controllers/common/helpers.py
  6. 3
    3
      api/controllers/console/app/annotation.py
  7. 119
    0
      api/controllers/console/app/generator.py
  8. 3
    5
      api/controllers/console/datasets/datasets_document.py
  9. 3
    1
      api/controllers/console/datasets/metadata.py
  10. 1
    1
      api/controllers/console/datasets/upload_file.py
  11. 3
    3
      api/controllers/service_api/app/annotation.py
  12. 4
    2
      api/controllers/service_api/dataset/dataset.py
  13. 3
    1
      api/controllers/service_api/dataset/metadata.py
  14. 1
    1
      api/core/app/task_pipeline/message_cycle_manager.py
  15. 184
    0
      api/core/llm_generator/llm_generator.py
  16. 113
    0
      api/core/llm_generator/prompts.py
  17. 12
    5
      api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py
  18. 7
    2
      api/core/rag/datasource/vdb/qdrant/qdrant_vector.py
  19. 8
    2
      api/extensions/ext_celery.py
  20. 1
    1
      api/pyproject.toml
  21. 155
    0
      api/schedule/clean_workflow_runlogs_precise.py
  22. 4
    4
      api/services/annotation_service.py
  23. 13
    10
      api/services/dataset_service.py
  24. 2
    1
      api/tasks/deal_dataset_vector_index_task.py
  25. 9
    0
      api/tests/integration_tests/vdb/qdrant/test_qdrant.py
  26. 2
    2
      api/tests/test_containers_integration_tests/services/test_annotation_service.py
  27. 1
    1
      api/tests/test_containers_integration_tests/services/test_api_based_extension_service.py
  28. 1785
    0
      api/tests/test_containers_integration_tests/services/test_feature_service.py
  29. 1
    1
      api/tests/test_containers_integration_tests/services/test_message_service.py
  30. 1
    1
      api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py
  31. 15
    4
      api/uv.lock
  32. 8
    0
      docker/.env.example
  33. 3
    0
      docker/docker-compose.yaml
  34. 47
    4
      web/app/account/account-page/AvatarWithEdit.tsx
  35. 10
    6
      web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx
  36. 156
    127
      web/app/components/app/configuration/config/automatic/get-automatic-res.tsx
  37. 48
    0
      web/app/components/app/configuration/config/automatic/idea-output.tsx
  38. 58
    0
      web/app/components/app/configuration/config/automatic/instruction-editor-in-workflow.tsx
  39. 117
    0
      web/app/components/app/configuration/config/automatic/instruction-editor.tsx
  40. 55
    0
      web/app/components/app/configuration/config/automatic/prompt-res-in-workflow.tsx
  41. 31
    0
      web/app/components/app/configuration/config/automatic/prompt-res.tsx
  42. 54
    0
      web/app/components/app/configuration/config/automatic/prompt-toast.tsx
  43. 18
    0
      web/app/components/app/configuration/config/automatic/res-placeholder.tsx
  44. 97
    0
      web/app/components/app/configuration/config/automatic/result.tsx
  45. 7
    1
      web/app/components/app/configuration/config/automatic/style.module.css
  46. 4
    0
      web/app/components/app/configuration/config/automatic/types.ts
  47. 36
    0
      web/app/components/app/configuration/config/automatic/use-gen-data.ts
  48. 103
    0
      web/app/components/app/configuration/config/automatic/version-selector.tsx
  49. 90
    84
      web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx
  50. 0
    43
      web/app/components/app/configuration/features/experience-enhance-group/index.tsx
  51. 0
    51
      web/app/components/app/configuration/features/experience-enhance-group/more-like-this/index.tsx
  52. 6
    0
      web/app/components/base/icons/assets/vender/line/general/code-assistant.svg
  53. 6
    0
      web/app/components/base/icons/assets/vender/line/general/magic-edit.svg
  54. 53
    0
      web/app/components/base/icons/src/vender/line/general/CodeAssistant.json
  55. 20
    0
      web/app/components/base/icons/src/vender/line/general/CodeAssistant.tsx
  56. 55
    0
      web/app/components/base/icons/src/vender/line/general/MagicEdit.json
  57. 20
    0
      web/app/components/base/icons/src/vender/line/general/MagicEdit.tsx
  58. 2
    0
      web/app/components/base/icons/src/vender/line/general/index.ts
  59. 4
    0
      web/app/components/base/prompt-editor/constants.tsx
  60. 66
    3
      web/app/components/base/prompt-editor/index.tsx
  61. 51
    3
      web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx
  62. 33
    5
      web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx
  63. 44
    0
      web/app/components/base/prompt-editor/plugins/current-block/component.tsx
  64. 61
    0
      web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx
  65. 66
    0
      web/app/components/base/prompt-editor/plugins/current-block/index.tsx
  66. 78
    0
      web/app/components/base/prompt-editor/plugins/current-block/node.tsx
  67. 40
    0
      web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx
  68. 60
    0
      web/app/components/base/prompt-editor/plugins/error-message-block/error-message-block-replacement-block.tsx
  69. 65
    0
      web/app/components/base/prompt-editor/plugins/error-message-block/index.tsx
  70. 67
    0
      web/app/components/base/prompt-editor/plugins/error-message-block/node.tsx
  71. 40
    0
      web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx
  72. 65
    0
      web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx
  73. 60
    0
      web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx
  74. 67
    0
      web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx
  75. 20
    0
      web/app/components/base/prompt-editor/types.ts
  76. 1
    1
      web/app/components/datasets/documents/detail/completed/common/tag.tsx
  77. 379
    4
      web/app/components/datasets/documents/list.tsx
  78. 1
    1
      web/app/components/explore/category.tsx
  79. 2
    2
      web/app/components/goto-anything/actions/commands/command-bus.ts
  80. 15
    0
      web/app/components/goto-anything/actions/commands/index.ts
  81. 53
    0
      web/app/components/goto-anything/actions/commands/language.tsx
  82. 233
    0
      web/app/components/goto-anything/actions/commands/registry.ts
  83. 52
    0
      web/app/components/goto-anything/actions/commands/slash.tsx
  84. 35
    16
      web/app/components/goto-anything/actions/commands/theme.tsx
  85. 33
    0
      web/app/components/goto-anything/actions/commands/types.ts
  86. 177
    5
      web/app/components/goto-anything/actions/index.ts
  87. 0
    33
      web/app/components/goto-anything/actions/run-language.tsx
  88. 0
    97
      web/app/components/goto-anything/actions/run.tsx
  89. 1
    1
      web/app/components/goto-anything/actions/types.ts
  90. 1
    1
      web/app/components/goto-anything/command-selector.tsx
  91. 16
    16
      web/app/components/goto-anything/index.tsx
  92. 1
    1
      web/app/components/header/account-dropdown/support.tsx
  93. 4
    0
      web/app/components/plugins/plugin-detail-panel/detail-header.tsx
  94. 1
    1
      web/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx
  95. 7
    2
      web/app/components/plugins/reference-setting-modal/modal.tsx
  96. 18
    2
      web/app/components/workflow-app/components/workflow-header/features-trigger.tsx
  97. 2
    1
      web/app/components/workflow/hooks/use-workflow-variables.ts
  98. 1
    0
      web/app/components/workflow/nodes/_base/components/agent-strategy.tsx
  99. 13
    3
      web/app/components/workflow/nodes/_base/components/code-generator-button.tsx
  100. 0
    0
      web/app/components/workflow/nodes/_base/components/editor/base.tsx

+ 3
- 0
.github/workflows/autofix.yml Zobrazit soubor

uv run ruff check --fix-only . uv run ruff check --fix-only .
# Format code # Format code
uv run ruff format . uv run ruff format .
- name: ast-grep
run: |
uvx --from ast-grep-cli sg --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all


- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27



+ 2
- 0
.gitignore Zobrazit soubor

!.vscode/README.md !.vscode/README.md
pyrightconfig.json pyrightconfig.json
api/.vscode api/.vscode
# vscode Code History Extension
.history


.idea/ .idea/



+ 7
- 0
api/.env.example Zobrazit soubor



# API workflow run repository implementation # API workflow run repository implementation
API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository
# Workflow log cleanup configuration
# Enable automatic cleanup of workflow run logs to manage database size
WORKFLOW_LOG_CLEANUP_ENABLED=true
# Number of days to retain workflow run logs (default: 30 days)
WORKFLOW_LOG_RETENTION_DAYS=30
# Batch size for workflow log cleanup operations (default: 100)
WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100


# App configuration # App configuration
APP_MAX_EXECUTION_TIME=1200 APP_MAX_EXECUTION_TIME=1200

+ 9
- 0
api/configs/feature/__init__.py Zobrazit soubor

) )




class WorkflowLogConfig(BaseSettings):
WORKFLOW_LOG_CLEANUP_ENABLED: bool = Field(default=True, description="Enable workflow run log cleanup")
WORKFLOW_LOG_RETENTION_DAYS: int = Field(default=30, description="Retention days for workflow run logs")
WORKFLOW_LOG_CLEANUP_BATCH_SIZE: int = Field(
default=100, description="Batch size for workflow run log cleanup operations"
)


class FeatureConfig( class FeatureConfig(
# place the configs in alphabet order # place the configs in alphabet order
AppExecutionConfig, AppExecutionConfig,
HostedServiceConfig, HostedServiceConfig,
CeleryBeatConfig, CeleryBeatConfig,
CeleryScheduleTasksConfig, CeleryScheduleTasksConfig,
WorkflowLogConfig,
): ):
pass pass

+ 2
- 3
api/controllers/common/helpers.py Zobrazit soubor

import contextlib
import mimetypes import mimetypes
import os import os
import platform import platform


# Use python-magic to guess MIME type if still unknown or generic # Use python-magic to guess MIME type if still unknown or generic
if mimetype == "application/octet-stream" and magic is not None: if mimetype == "application/octet-stream" and magic is not None:
try:
with contextlib.suppress(magic.MagicException):
mimetype = magic.from_buffer(response.content[:1024], mime=True) mimetype = magic.from_buffer(response.content[:1024], mime=True)
except magic.MagicException:
pass


extension = os.path.splitext(filename)[1] extension = os.path.splitext(filename)[1]



+ 3
- 3
api/controllers/console/app/annotation.py Zobrazit soubor

from typing import Literal

from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, marshal, marshal_with, reqparse from flask_restful import Resource, marshal, marshal_with, reqparse
@login_required @login_required
@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("annotation") @cloud_edition_billing_resource_check("annotation")
def post(self, app_id, action):
def post(self, app_id, action: Literal["enable", "disable"]):
if not current_user.is_editor: if not current_user.is_editor:
raise Forbidden() raise Forbidden()


result = AppAnnotationService.enable_app_annotation(args, app_id) result = AppAnnotationService.enable_app_annotation(args, app_id)
elif action == "disable": elif action == "disable":
result = AppAnnotationService.disable_app_annotation(app_id) result = AppAnnotationService.disable_app_annotation(app_id)
else:
raise ValueError("Unsupported annotation reply action")
return result, 200 return result, 200





+ 119
- 0
api/controllers/console/app/generator.py Zobrazit soubor

from collections.abc import Sequence

from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, reqparse from flask_restful import Resource, reqparse


) )
from controllers.console.wraps import account_initialization_required, setup_required from controllers.console.wraps import account_initialization_required, setup_required
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
from core.helper.code_executor.javascript.javascript_code_provider import JavascriptCodeProvider
from core.helper.code_executor.python3.python3_code_provider import Python3CodeProvider
from core.llm_generator.llm_generator import LLMGenerator from core.llm_generator.llm_generator import LLMGenerator
from core.model_runtime.errors.invoke import InvokeError from core.model_runtime.errors.invoke import InvokeError
from libs.login import login_required from libs.login import login_required
return structured_output return structured_output




class InstructionGenerateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("flow_id", type=str, required=True, default="", location="json")
parser.add_argument("node_id", type=str, required=False, default="", location="json")
parser.add_argument("current", type=str, required=False, default="", location="json")
parser.add_argument("language", type=str, required=False, default="javascript", location="json")
parser.add_argument("instruction", type=str, required=True, nullable=False, location="json")
parser.add_argument("model_config", type=dict, required=True, nullable=False, location="json")
parser.add_argument("ideal_output", type=str, required=False, default="", location="json")
args = parser.parse_args()
code_template = (
Python3CodeProvider.get_default_code()
if args["language"] == "python"
else (JavascriptCodeProvider.get_default_code())
if args["language"] == "javascript"
else ""
)
try:
# Generate from nothing for a workflow node
if (args["current"] == code_template or args["current"] == "") and args["node_id"] != "":
from models import App, db
from services.workflow_service import WorkflowService

app = db.session.query(App).where(App.id == args["flow_id"]).first()
if not app:
return {"error": f"app {args['flow_id']} not found"}, 400
workflow = WorkflowService().get_draft_workflow(app_model=app)
if not workflow:
return {"error": f"workflow {args['flow_id']} not found"}, 400
nodes: Sequence = workflow.graph_dict["nodes"]
node = [node for node in nodes if node["id"] == args["node_id"]]
if len(node) == 0:
return {"error": f"node {args['node_id']} not found"}, 400
node_type = node[0]["data"]["type"]
match node_type:
case "llm":
return LLMGenerator.generate_rule_config(
current_user.current_tenant_id,
instruction=args["instruction"],
model_config=args["model_config"],
no_variable=True,
)
case "agent":
return LLMGenerator.generate_rule_config(
current_user.current_tenant_id,
instruction=args["instruction"],
model_config=args["model_config"],
no_variable=True,
)
case "code":
return LLMGenerator.generate_code(
tenant_id=current_user.current_tenant_id,
instruction=args["instruction"],
model_config=args["model_config"],
code_language=args["language"],
)
case _:
return {"error": f"invalid node type: {node_type}"}
if args["node_id"] == "" and args["current"] != "": # For legacy app without a workflow
return LLMGenerator.instruction_modify_legacy(
tenant_id=current_user.current_tenant_id,
flow_id=args["flow_id"],
current=args["current"],
instruction=args["instruction"],
model_config=args["model_config"],
ideal_output=args["ideal_output"],
)
if args["node_id"] != "" and args["current"] != "": # For workflow node
return LLMGenerator.instruction_modify_workflow(
tenant_id=current_user.current_tenant_id,
flow_id=args["flow_id"],
node_id=args["node_id"],
current=args["current"],
instruction=args["instruction"],
model_config=args["model_config"],
ideal_output=args["ideal_output"],
)
return {"error": "incompatible parameters"}, 400
except ProviderTokenNotInitError as ex:
raise ProviderNotInitializeError(ex.description)
except QuotaExceededError:
raise ProviderQuotaExceededError()
except ModelCurrentlyNotSupportError:
raise ProviderModelCurrentlyNotSupportError()
except InvokeError as e:
raise CompletionRequestError(e.description)


class InstructionGenerationTemplateApi(Resource):
@setup_required
@login_required
@account_initialization_required
def post(self) -> dict:
parser = reqparse.RequestParser()
parser.add_argument("type", type=str, required=True, default=False, location="json")
args = parser.parse_args()
match args["type"]:
case "prompt":
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_PROMPT

return {"data": INSTRUCTION_GENERATE_TEMPLATE_PROMPT}
case "code":
from core.llm_generator.prompts import INSTRUCTION_GENERATE_TEMPLATE_CODE

return {"data": INSTRUCTION_GENERATE_TEMPLATE_CODE}
case _:
raise ValueError(f"Invalid type: {args['type']}")


api.add_resource(RuleGenerateApi, "/rule-generate") api.add_resource(RuleGenerateApi, "/rule-generate")
api.add_resource(RuleCodeGenerateApi, "/rule-code-generate") api.add_resource(RuleCodeGenerateApi, "/rule-code-generate")
api.add_resource(RuleStructuredOutputGenerateApi, "/rule-structured-output-generate") api.add_resource(RuleStructuredOutputGenerateApi, "/rule-structured-output-generate")
api.add_resource(InstructionGenerateApi, "/instruction-generate")
api.add_resource(InstructionGenerationTemplateApi, "/instruction-generate/template")

+ 3
- 5
api/controllers/console/datasets/datasets_document.py Zobrazit soubor

import json import json
import logging import logging
from argparse import ArgumentTypeError from argparse import ArgumentTypeError
from typing import cast
from typing import Literal, cast


from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
@login_required @login_required
@account_initialization_required @account_initialization_required
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, document_id, action):
def patch(self, dataset_id, document_id, action: Literal["pause", "resume"]):
dataset_id = str(dataset_id) dataset_id = str(dataset_id)
document_id = str(document_id) document_id = str(document_id)
document = self.get_document(dataset_id, document_id) document = self.get_document(dataset_id, document_id)
document.paused_at = None document.paused_at = None
document.is_paused = False document.is_paused = False
db.session.commit() db.session.commit()
else:
raise InvalidActionError()


return {"result": "success"}, 200 return {"result": "success"}, 200


@account_initialization_required @account_initialization_required
@cloud_edition_billing_resource_check("vector_space") @cloud_edition_billing_resource_check("vector_space")
@cloud_edition_billing_rate_limit_check("knowledge") @cloud_edition_billing_rate_limit_check("knowledge")
def patch(self, dataset_id, action):
def patch(self, dataset_id, action: Literal["enable", "disable", "archive", "un_archive"]):
dataset_id = str(dataset_id) dataset_id = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id) dataset = DatasetService.get_dataset(dataset_id)
if dataset is None: if dataset is None:

+ 3
- 1
api/controllers/console/datasets/metadata.py Zobrazit soubor

from typing import Literal

from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, marshal_with, reqparse from flask_restful import Resource, marshal_with, reqparse
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
@login_required @login_required
@account_initialization_required @account_initialization_required
@enterprise_license_required @enterprise_license_required
def post(self, dataset_id, action):
def post(self, dataset_id, action: Literal["enable", "disable"]):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str) dataset = DatasetService.get_dataset(dataset_id_str)
if dataset is None: if dataset is None:

+ 1
- 1
api/controllers/console/datasets/upload_file.py Zobrazit soubor

data_source_info = document.data_source_info_dict data_source_info = document.data_source_info_dict
if data_source_info and "upload_file_id" in data_source_info: if data_source_info and "upload_file_id" in data_source_info:
file_id = data_source_info["upload_file_id"] file_id = data_source_info["upload_file_id"]
upload_file = db.session.query(UploadFile).filter(UploadFile.id == file_id).first()
upload_file = db.session.query(UploadFile).where(UploadFile.id == file_id).first()
if not upload_file: if not upload_file:
raise NotFound("UploadFile not found.") raise NotFound("UploadFile not found.")
else: else:

+ 3
- 3
api/controllers/service_api/app/annotation.py Zobrazit soubor

from typing import Literal

from flask import request from flask import request
from flask_restful import Resource, marshal, marshal_with, reqparse from flask_restful import Resource, marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden from werkzeug.exceptions import Forbidden


class AnnotationReplyActionApi(Resource): class AnnotationReplyActionApi(Resource):
@validate_app_token @validate_app_token
def post(self, app_model: App, action):
def post(self, app_model: App, action: Literal["enable", "disable"]):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("score_threshold", required=True, type=float, location="json") parser.add_argument("score_threshold", required=True, type=float, location="json")
parser.add_argument("embedding_provider_name", required=True, type=str, location="json") parser.add_argument("embedding_provider_name", required=True, type=str, location="json")
result = AppAnnotationService.enable_app_annotation(args, app_model.id) result = AppAnnotationService.enable_app_annotation(args, app_model.id)
elif action == "disable": elif action == "disable":
result = AppAnnotationService.disable_app_annotation(app_model.id) result = AppAnnotationService.disable_app_annotation(app_model.id)
else:
raise ValueError("Unsupported annotation reply action")
return result, 200 return result, 200





+ 4
- 2
api/controllers/service_api/dataset/dataset.py Zobrazit soubor

from typing import Literal

from flask import request from flask import request
from flask_restful import marshal, marshal_with, reqparse from flask_restful import marshal, marshal_with, reqparse
from werkzeug.exceptions import Forbidden, NotFound from werkzeug.exceptions import Forbidden, NotFound
class DocumentStatusApi(DatasetApiResource): class DocumentStatusApi(DatasetApiResource):
"""Resource for batch document status operations.""" """Resource for batch document status operations."""


def patch(self, tenant_id, dataset_id, action):
def patch(self, tenant_id, dataset_id, action: Literal["enable", "disable", "archive", "un_archive"]):
""" """
Batch update document status. Batch update document status.


Args: Args:
tenant_id: tenant id tenant_id: tenant id
dataset_id: dataset id dataset_id: dataset id
action: action to perform (enable, disable, archive, un_archive)
action: action to perform (Literal["enable", "disable", "archive", "un_archive"])


Returns: Returns:
dict: A dictionary with a key 'result' and a value 'success' dict: A dictionary with a key 'result' and a value 'success'

+ 3
- 1
api/controllers/service_api/dataset/metadata.py Zobrazit soubor

from typing import Literal

from flask_login import current_user # type: ignore from flask_login import current_user # type: ignore
from flask_restful import marshal, reqparse from flask_restful import marshal, reqparse
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound


class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource): class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource):
@cloud_edition_billing_rate_limit_check("knowledge", "dataset") @cloud_edition_billing_rate_limit_check("knowledge", "dataset")
def post(self, tenant_id, dataset_id, action):
def post(self, tenant_id, dataset_id, action: Literal["enable", "disable"]):
dataset_id_str = str(dataset_id) dataset_id_str = str(dataset_id)
dataset = DatasetService.get_dataset(dataset_id_str) dataset = DatasetService.get_dataset(dataset_id_str)
if dataset is None: if dataset is None:

+ 1
- 1
api/core/app/task_pipeline/message_cycle_manager.py Zobrazit soubor

:param message_id: message id :param message_id: message id
:return: :return:
""" """
message_file = db.session.query(MessageFile).filter(MessageFile.id == message_id).first()
message_file = db.session.query(MessageFile).where(MessageFile.id == message_id).first()
event_type = StreamEvent.MESSAGE_FILE if message_file else StreamEvent.MESSAGE event_type = StreamEvent.MESSAGE_FILE if message_file else StreamEvent.MESSAGE


return MessageStreamResponse( return MessageStreamResponse(

+ 184
- 0
api/core/llm_generator/llm_generator.py Zobrazit soubor

import json import json
import logging import logging
import re import re
from collections.abc import Sequence
from typing import Optional, cast from typing import Optional, cast


import json_repair import json_repair
CONVERSATION_TITLE_PROMPT, CONVERSATION_TITLE_PROMPT,
GENERATOR_QA_PROMPT, GENERATOR_QA_PROMPT,
JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE, JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE,
LLM_MODIFY_CODE_SYSTEM,
LLM_MODIFY_PROMPT_SYSTEM,
PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE, PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE,
SYSTEM_STRUCTURED_OUTPUT_GENERATE, SYSTEM_STRUCTURED_OUTPUT_GENERATE,
WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE, WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE,
from core.ops.ops_trace_manager import TraceQueueManager, TraceTask from core.ops.ops_trace_manager import TraceQueueManager, TraceTask
from core.ops.utils import measure_time from core.ops.utils import measure_time
from core.prompt.utils.prompt_template_parser import PromptTemplateParser from core.prompt.utils.prompt_template_parser import PromptTemplateParser
from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey
from core.workflow.graph_engine.entities.event import AgentLogEvent
from models import App, Message, WorkflowNodeExecutionModel, db




class LLMGenerator: class LLMGenerator:
except Exception as e: except Exception as e:
logging.exception("Failed to invoke LLM model, model: %s", model_config.get("name")) logging.exception("Failed to invoke LLM model, model: %s", model_config.get("name"))
return {"output": "", "error": f"An unexpected error occurred: {str(e)}"} return {"output": "", "error": f"An unexpected error occurred: {str(e)}"}

@staticmethod
def instruction_modify_legacy(
tenant_id: str, flow_id: str, current: str, instruction: str, model_config: dict, ideal_output: str | None
) -> dict:
app: App | None = db.session.query(App).where(App.id == flow_id).first()
last_run: Message | None = (
db.session.query(Message).where(Message.app_id == flow_id).order_by(Message.created_at.desc()).first()
)
if not last_run:
return LLMGenerator.__instruction_modify_common(
tenant_id=tenant_id,
model_config=model_config,
last_run=None,
current=current,
error_message="",
instruction=instruction,
node_type="llm",
ideal_output=ideal_output,
)
last_run_dict = {
"query": last_run.query,
"answer": last_run.answer,
"error": last_run.error,
}
return LLMGenerator.__instruction_modify_common(
tenant_id=tenant_id,
model_config=model_config,
last_run=last_run_dict,
current=current,
error_message=str(last_run.error),
instruction=instruction,
node_type="llm",
ideal_output=ideal_output,
)

@staticmethod
def instruction_modify_workflow(
tenant_id: str,
flow_id: str,
node_id: str,
current: str,
instruction: str,
model_config: dict,
ideal_output: str | None,
) -> dict:
from services.workflow_service import WorkflowService

app: App | None = db.session.query(App).where(App.id == flow_id).first()
if not app:
raise ValueError("App not found.")
workflow = WorkflowService().get_draft_workflow(app_model=app)
if not workflow:
raise ValueError("Workflow not found for the given app model.")
last_run = WorkflowService().get_node_last_run(app_model=app, workflow=workflow, node_id=node_id)
try:
node_type = cast(WorkflowNodeExecutionModel, last_run).node_type
except Exception:
try:
node_type = [it for it in workflow.graph_dict["graph"]["nodes"] if it["id"] == node_id][0]["data"][
"type"
]
except Exception:
node_type = "llm"

if not last_run: # Node is not executed yet
return LLMGenerator.__instruction_modify_common(
tenant_id=tenant_id,
model_config=model_config,
last_run=None,
current=current,
error_message="",
instruction=instruction,
node_type=node_type,
ideal_output=ideal_output,
)

def agent_log_of(node_execution: WorkflowNodeExecutionModel) -> Sequence:
raw_agent_log = node_execution.execution_metadata_dict.get(WorkflowNodeExecutionMetadataKey.AGENT_LOG)
if not raw_agent_log:
return []
parsed: Sequence[AgentLogEvent] = json.loads(raw_agent_log)

def dict_of_event(event: AgentLogEvent) -> dict:
return {
"status": event.status,
"error": event.error,
"data": event.data,
}

return [dict_of_event(event) for event in parsed]

last_run_dict = {
"inputs": last_run.inputs_dict,
"status": last_run.status,
"error": last_run.error,
"agent_log": agent_log_of(last_run),
}

return LLMGenerator.__instruction_modify_common(
tenant_id=tenant_id,
model_config=model_config,
last_run=last_run_dict,
current=current,
error_message=last_run.error,
instruction=instruction,
node_type=last_run.node_type,
ideal_output=ideal_output,
)

@staticmethod
def __instruction_modify_common(
tenant_id: str,
model_config: dict,
last_run: dict | None,
current: str | None,
error_message: str | None,
instruction: str,
node_type: str,
ideal_output: str | None,
) -> dict:
LAST_RUN = "{{#last_run#}}"
CURRENT = "{{#current#}}"
ERROR_MESSAGE = "{{#error_message#}}"
injected_instruction = instruction
if LAST_RUN in injected_instruction:
injected_instruction = injected_instruction.replace(LAST_RUN, json.dumps(last_run))
if CURRENT in injected_instruction:
injected_instruction = injected_instruction.replace(CURRENT, current or "null")
if ERROR_MESSAGE in injected_instruction:
injected_instruction = injected_instruction.replace(ERROR_MESSAGE, error_message or "null")
model_instance = ModelManager().get_model_instance(
tenant_id=tenant_id,
model_type=ModelType.LLM,
provider=model_config.get("provider", ""),
model=model_config.get("name", ""),
)
match node_type:
case "llm", "agent":
system_prompt = LLM_MODIFY_PROMPT_SYSTEM
case "code":
system_prompt = LLM_MODIFY_CODE_SYSTEM
case _:
system_prompt = LLM_MODIFY_PROMPT_SYSTEM
prompt_messages = [
SystemPromptMessage(content=system_prompt),
UserPromptMessage(
content=json.dumps(
{
"current": current,
"last_run": last_run,
"instruction": injected_instruction,
"ideal_output": ideal_output,
}
)
),
]
model_parameters = {"temperature": 0.4}

try:
response = cast(
LLMResult,
model_instance.invoke_llm(
prompt_messages=list(prompt_messages), model_parameters=model_parameters, stream=False
),
)

generated_raw = cast(str, response.message.content)
first_brace = generated_raw.find("{")
last_brace = generated_raw.rfind("}")
return {**json.loads(generated_raw[first_brace : last_brace + 1])}

except InvokeError as e:
error = str(e)
return {"error": f"Failed to generate code. Error: {error}"}
except Exception as e:
logging.exception("Failed to invoke LLM model, model: " + json.dumps(model_config.get("name")), exc_info=e)
return {"error": f"An unexpected error occurred: {str(e)}"}

+ 113
- 0
api/core/llm_generator/prompts.py Zobrazit soubor

Here is the JSON schema: Here is the JSON schema:
{{schema}} {{schema}}
""" # noqa: E501 """ # noqa: E501

LLM_MODIFY_PROMPT_SYSTEM = """
Both your input and output should be in JSON format.

! Below is the schema for input content !
{
"type": "object",
"description": "The user is trying to process some content with a prompt, but the output is not as expected. They hope to achieve their goal by modifying the prompt.",
"properties": {
"current": {
"type": "string",
"description": "The prompt before modification, where placeholders {{}} will be replaced with actual values for the large language model. The content in the placeholders should not be changed."
},
"last_run": {
"type": "object",
"description": "The output result from the large language model after receiving the prompt.",
},
"instruction": {
"type": "string",
"description": "User's instruction to edit the current prompt"
},
"ideal_output": {
"type": "string",
"description": "The ideal output that the user expects from the large language model after modifying the prompt. You should compare the last output with the ideal output and make changes to the prompt to achieve the goal."
}
}
}
! Above is the schema for input content !

! Below is the schema for output content !
{
"type": "object",
"description": "Your feedback to the user after they provide modification suggestions.",
"properties": {
"modified": {
"type": "string",
"description": "Your modified prompt. You should change the original prompt as little as possible to achieve the goal. Keep the language of prompt if not asked to change"
},
"message": {
"type": "string",
"description": "Your feedback to the user, in the user's language, explaining what you did and your thought process in text, providing sufficient emotional value to the user."
}
},
"required": [
"modified",
"message"
]
}
! Above is the schema for output content !

Your output must strictly follow the schema format, do not output any content outside of the JSON body.
""" # noqa: E501

LLM_MODIFY_CODE_SYSTEM = """
Both your input and output should be in JSON format.

! Below is the schema for input content !
{
"type": "object",
"description": "The user is trying to process some data with a code snippet, but the result is not as expected. They hope to achieve their goal by modifying the code.",
"properties": {
"current": {
"type": "string",
"description": "The code before modification."
},
"last_run": {
"type": "object",
"description": "The result of the code.",
},
"message": {
"type": "string",
"description": "User's instruction to edit the current code"
}
}
}
! Above is the schema for input content !

! Below is the schema for output content !
{
"type": "object",
"description": "Your feedback to the user after they provide modification suggestions.",
"properties": {
"modified": {
"type": "string",
"description": "Your modified code. You should change the original code as little as possible to achieve the goal. Keep the programming language of code if not asked to change"
},
"message": {
"type": "string",
"description": "Your feedback to the user, in the user's language, explaining what you did and your thought process in text, providing sufficient emotional value to the user."
}
},
"required": [
"modified",
"message"
]
}
! Above is the schema for output content !

When you are modifying the code, you should remember:
- Do not use print, this not work in dify sandbox.
- Do not try dangerous call like deleting files. It's PROHIBITED.
- Do not use any library that is not built-in in with Python.
- Get inputs from the parameters of the function and have explicit type annotations.
- Write proper imports at the top of the code.
- Use return statement to return the result.
- You should return a `dict`. If you need to return a `result: str`, you should `return {"result": result}`.
Your output must strictly follow the schema format, do not output any content outside of the JSON body.
""" # noqa: E501

INSTRUCTION_GENERATE_TEMPLATE_PROMPT = """The output of this prompt is not as expected: {{#last_run#}}.
You should edit the prompt according to the IDEAL OUTPUT."""

INSTRUCTION_GENERATE_TEMPLATE_CODE = """Please fix the errors in the {{#error_message#}}."""

+ 12
- 5
api/core/rag/datasource/vdb/oceanbase/oceanbase_vector.py Zobrazit soubor

from typing import Any from typing import Any


from pydantic import BaseModel, model_validator from pydantic import BaseModel, model_validator
from pyobvector import VECTOR, ObVecClient # type: ignore
from sqlalchemy import JSON, Column, String, func
from pyobvector import VECTOR, FtsIndexParam, FtsParser, ObVecClient, l2_distance # type: ignore
from sqlalchemy import JSON, Column, String
from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.dialects.mysql import LONGTEXT


from configs import dify_config from configs import dify_config
) )
try: try:
if self._hybrid_search_enabled: if self._hybrid_search_enabled:
self._client.perform_raw_text_sql(f"""ALTER TABLE {self._collection_name}
ADD FULLTEXT INDEX fulltext_index_for_col_text (text) WITH PARSER ik""")
self._client.create_fts_idx_with_fts_index_param(
table_name=self._collection_name,
fts_idx_param=FtsIndexParam(
index_name="fulltext_index_for_col_text",
field_names=["text"],
parser_type=FtsParser.IK,
),
)
except Exception as e: except Exception as e:
raise Exception( raise Exception(
"Failed to add fulltext index to the target table, your OceanBase version must be 4.3.5.1 or above " "Failed to add fulltext index to the target table, your OceanBase version must be 4.3.5.1 or above "
+ "to support fulltext index and vector index in the same table", + "to support fulltext index and vector index in the same table",
e, e,
) )
self._client.refresh_metadata([self._collection_name])
redis_client.set(collection_exist_cache_key, 1, ex=3600) redis_client.set(collection_exist_cache_key, 1, ex=3600)


def _check_hybrid_search_support(self) -> bool: def _check_hybrid_search_support(self) -> bool:
vec_column_name="vector", vec_column_name="vector",
vec_data=query_vector, vec_data=query_vector,
topk=topk, topk=topk,
distance_func=func.l2_distance,
distance_func=l2_distance,
output_column_names=["text", "metadata"], output_column_names=["text", "metadata"],
with_dist=True, with_dist=True,
where_clause=_where_clause, where_clause=_where_clause,

+ 7
- 2
api/core/rag/datasource/vdb/qdrant/qdrant_vector.py Zobrazit soubor

def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]: def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]:
from qdrant_client.http import models from qdrant_client.http import models


score_threshold = float(kwargs.get("score_threshold") or 0.0)
if score_threshold >= 1:
# return empty list because some versions of qdrant may response with 400 bad request,
# and at the same time, the score_threshold with value 1 may be valid for other vector stores
return []

filter = models.Filter( filter = models.Filter(
must=[ must=[
models.FieldCondition( models.FieldCondition(
limit=kwargs.get("top_k", 4), limit=kwargs.get("top_k", 4),
with_payload=True, with_payload=True,
with_vectors=True, with_vectors=True,
score_threshold=float(kwargs.get("score_threshold") or 0.0),
score_threshold=score_threshold,
) )
docs = [] docs = []
for result in results: for result in results:
continue continue
metadata = result.payload.get(Field.METADATA_KEY.value) or {} metadata = result.payload.get(Field.METADATA_KEY.value) or {}
# duplicate check score threshold # duplicate check score threshold
score_threshold = float(kwargs.get("score_threshold") or 0.0)
if result.score > score_threshold: if result.score > score_threshold:
metadata["score"] = result.score metadata["score"] = result.score
doc = Document( doc = Document(

+ 8
- 2
api/extensions/ext_celery.py Zobrazit soubor

minutes=dify_config.QUEUE_MONITOR_INTERVAL if dify_config.QUEUE_MONITOR_INTERVAL else 30 minutes=dify_config.QUEUE_MONITOR_INTERVAL if dify_config.QUEUE_MONITOR_INTERVAL else 30
), ),
} }
if dify_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK:
if dify_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK and dify_config.MARKETPLACE_ENABLED:
imports.append("schedule.check_upgradable_plugin_task") imports.append("schedule.check_upgradable_plugin_task")
beat_schedule["check_upgradable_plugin_task"] = { beat_schedule["check_upgradable_plugin_task"] = {
"task": "schedule.check_upgradable_plugin_task.check_upgradable_plugin_task", "task": "schedule.check_upgradable_plugin_task.check_upgradable_plugin_task",
"schedule": crontab(minute="*/15"), "schedule": crontab(minute="*/15"),
} }

if dify_config.WORKFLOW_LOG_CLEANUP_ENABLED:
# 2:00 AM every day
imports.append("schedule.clean_workflow_runlogs_precise")
beat_schedule["clean_workflow_runlogs_precise"] = {
"task": "schedule.clean_workflow_runlogs_precise.clean_workflow_runlogs_precise",
"schedule": crontab(minute="0", hour="2"),
}
celery_app.conf.update(beat_schedule=beat_schedule, imports=imports) celery_app.conf.update(beat_schedule=beat_schedule, imports=imports)


return celery_app return celery_app

+ 1
- 1
api/pyproject.toml Zobrazit soubor

"pgvector==0.2.5", "pgvector==0.2.5",
"pymilvus~=2.5.0", "pymilvus~=2.5.0",
"pymochow==1.3.1", "pymochow==1.3.1",
"pyobvector~=0.1.6",
"pyobvector~=0.2.15",
"qdrant-client==1.9.0", "qdrant-client==1.9.0",
"tablestore==6.2.0", "tablestore==6.2.0",
"tcvectordb~=1.6.4", "tcvectordb~=1.6.4",

+ 155
- 0
api/schedule/clean_workflow_runlogs_precise.py Zobrazit soubor

import datetime
import logging
import time

import click

import app
from configs import dify_config
from extensions.ext_database import db
from models.model import (
AppAnnotationHitHistory,
Conversation,
Message,
MessageAgentThought,
MessageAnnotation,
MessageChain,
MessageFeedback,
MessageFile,
)
from models.workflow import ConversationVariable, WorkflowAppLog, WorkflowNodeExecutionModel, WorkflowRun

_logger = logging.getLogger(__name__)


MAX_RETRIES = 3
BATCH_SIZE = dify_config.WORKFLOW_LOG_CLEANUP_BATCH_SIZE


@app.celery.task(queue="dataset")
def clean_workflow_runlogs_precise():
"""Clean expired workflow run logs with retry mechanism and complete message cascade"""

click.echo(click.style("Start clean workflow run logs (precise mode with complete cascade).", fg="green"))
start_at = time.perf_counter()

retention_days = dify_config.WORKFLOW_LOG_RETENTION_DAYS
cutoff_date = datetime.datetime.now() - datetime.timedelta(days=retention_days)

try:
total_workflow_runs = db.session.query(WorkflowRun).where(WorkflowRun.created_at < cutoff_date).count()
if total_workflow_runs == 0:
_logger.info("No expired workflow run logs found")
return
_logger.info("Found %s expired workflow run logs to clean", total_workflow_runs)

total_deleted = 0
failed_batches = 0
batch_count = 0

while True:
workflow_runs = (
db.session.query(WorkflowRun.id).where(WorkflowRun.created_at < cutoff_date).limit(BATCH_SIZE).all()
)

if not workflow_runs:
break

workflow_run_ids = [run.id for run in workflow_runs]
batch_count += 1

success = _delete_batch_with_retry(workflow_run_ids, failed_batches)

if success:
total_deleted += len(workflow_run_ids)
failed_batches = 0
else:
failed_batches += 1
if failed_batches >= MAX_RETRIES:
_logger.error("Failed to delete batch after %s retries, aborting cleanup for today", MAX_RETRIES)
break
else:
# Calculate incremental delay times: 5, 10, 15 minutes
retry_delay_minutes = failed_batches * 5
_logger.warning("Batch deletion failed, retrying in %s minutes...", retry_delay_minutes)
time.sleep(retry_delay_minutes * 60)
continue

_logger.info("Cleanup completed: %s expired workflow run logs deleted", total_deleted)

except Exception as e:
db.session.rollback()
_logger.exception("Unexpected error in workflow log cleanup")
raise

end_at = time.perf_counter()
execution_time = end_at - start_at
click.echo(click.style(f"Cleaned workflow run logs from db success latency: {execution_time:.2f}s", fg="green"))


def _delete_batch_with_retry(workflow_run_ids: list[str], attempt_count: int) -> bool:
"""Delete a single batch with a retry mechanism and complete cascading deletion"""
try:
with db.session.begin_nested():
message_data = (
db.session.query(Message.id, Message.conversation_id)
.filter(Message.workflow_run_id.in_(workflow_run_ids))
.all()
)
message_id_list = [msg.id for msg in message_data]
conversation_id_list = list({msg.conversation_id for msg in message_data if msg.conversation_id})
if message_id_list:
db.session.query(AppAnnotationHitHistory).where(
AppAnnotationHitHistory.message_id.in_(message_id_list)
).delete(synchronize_session=False)

db.session.query(MessageAgentThought).where(MessageAgentThought.message_id.in_(message_id_list)).delete(
synchronize_session=False
)

db.session.query(MessageChain).where(MessageChain.message_id.in_(message_id_list)).delete(
synchronize_session=False
)

db.session.query(MessageFile).where(MessageFile.message_id.in_(message_id_list)).delete(
synchronize_session=False
)

db.session.query(MessageAnnotation).where(MessageAnnotation.message_id.in_(message_id_list)).delete(
synchronize_session=False
)

db.session.query(MessageFeedback).where(MessageFeedback.message_id.in_(message_id_list)).delete(
synchronize_session=False
)

db.session.query(Message).where(Message.workflow_run_id.in_(workflow_run_ids)).delete(
synchronize_session=False
)

db.session.query(WorkflowAppLog).where(WorkflowAppLog.workflow_run_id.in_(workflow_run_ids)).delete(
synchronize_session=False
)

db.session.query(WorkflowNodeExecutionModel).where(
WorkflowNodeExecutionModel.workflow_run_id.in_(workflow_run_ids)
).delete(synchronize_session=False)

if conversation_id_list:
db.session.query(ConversationVariable).where(
ConversationVariable.conversation_id.in_(conversation_id_list)
).delete(synchronize_session=False)

db.session.query(Conversation).where(Conversation.id.in_(conversation_id_list)).delete(
synchronize_session=False
)

db.session.query(WorkflowRun).where(WorkflowRun.id.in_(workflow_run_ids)).delete(synchronize_session=False)

db.session.commit()
return True

except Exception as e:
db.session.rollback()
_logger.exception("Batch deletion failed (attempt %s)", attempt_count + 1)
return False

+ 4
- 4
api/services/annotation_service.py Zobrazit soubor

annotation_ids_to_delete = [annotation.id for annotation, _ in annotations_to_delete] annotation_ids_to_delete = [annotation.id for annotation, _ in annotations_to_delete]


# Step 2: Bulk delete hit histories in a single query # Step 2: Bulk delete hit histories in a single query
db.session.query(AppAnnotationHitHistory).filter(
db.session.query(AppAnnotationHitHistory).where(
AppAnnotationHitHistory.annotation_id.in_(annotation_ids_to_delete) AppAnnotationHitHistory.annotation_id.in_(annotation_ids_to_delete)
).delete(synchronize_session=False) ).delete(synchronize_session=False)


# Step 4: Bulk delete annotations in a single query # Step 4: Bulk delete annotations in a single query
deleted_count = ( deleted_count = (
db.session.query(MessageAnnotation) db.session.query(MessageAnnotation)
.filter(MessageAnnotation.id.in_(annotation_ids_to_delete))
.where(MessageAnnotation.id.in_(annotation_ids_to_delete))
.delete(synchronize_session=False) .delete(synchronize_session=False)
) )


db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first() db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
) )


annotations_query = db.session.query(MessageAnnotation).filter(MessageAnnotation.app_id == app_id)
annotations_query = db.session.query(MessageAnnotation).where(MessageAnnotation.app_id == app_id)
for annotation in annotations_query.yield_per(100): for annotation in annotations_query.yield_per(100):
annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).filter(
annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).where(
AppAnnotationHitHistory.annotation_id == annotation.id AppAnnotationHitHistory.annotation_id == annotation.id
) )
for annotation_hit_history in annotation_hit_histories_query.yield_per(100): for annotation_hit_history in annotation_hit_histories_query.yield_per(100):

+ 13
- 10
api/services/dataset_service.py Zobrazit soubor

import time import time
import uuid import uuid
from collections import Counter from collections import Counter
from typing import Any, Optional
from typing import Any, Literal, Optional


from flask_login import current_user from flask_login import current_user
from sqlalchemy import func, select from sqlalchemy import func, select
KnowledgeConfiguration, KnowledgeConfiguration,
RagPipelineDatasetCreateEntity, RagPipelineDatasetCreateEntity,
) )
from services.errors.account import InvalidActionError, NoPermissionError
from services.errors.account import NoPermissionError
from services.errors.chunk import ChildChunkDeleteIndexError, ChildChunkIndexingError from services.errors.chunk import ChildChunkDeleteIndexError, ChildChunkIndexingError
from services.errors.dataset import DatasetNameDuplicateError from services.errors.dataset import DatasetNameDuplicateError
from services.errors.document import DocumentIndexingError from services.errors.document import DocumentIndexingError
raise ValueError("Process rule segmentation max_tokens is invalid") raise ValueError("Process rule segmentation max_tokens is invalid")


@staticmethod @staticmethod
def batch_update_document_status(dataset: Dataset, document_ids: list[str], action: str, user):
def batch_update_document_status(
dataset: Dataset, document_ids: list[str], action: Literal["enable", "disable", "archive", "un_archive"], user
):
""" """
Batch update document status. Batch update document status.


Args: Args:
dataset (Dataset): The dataset object dataset (Dataset): The dataset object
document_ids (list[str]): List of document IDs to update document_ids (list[str]): List of document IDs to update
action (str): Action to perform (enable, disable, archive, un_archive)
action (Literal["enable", "disable", "archive", "un_archive"]): Action to perform
user: Current user performing the action user: Current user performing the action


Raises: Raises:
raise propagation_error raise propagation_error


@staticmethod @staticmethod
def _prepare_document_status_update(document, action: str, user):
"""
Prepare document status update information.
def _prepare_document_status_update(
document: Document, action: Literal["enable", "disable", "archive", "un_archive"], user
):
"""Prepare document status update information.


Args: Args:
document: Document object to update document: Document object to update
db.session.commit() db.session.commit()


@classmethod @classmethod
def update_segments_status(cls, segment_ids: list, action: str, dataset: Dataset, document: Document):
def update_segments_status(
cls, segment_ids: list, action: Literal["enable", "disable"], dataset: Dataset, document: Document
):
# Check if segment_ids is not empty to avoid WHERE false condition # Check if segment_ids is not empty to avoid WHERE false condition
if not segment_ids or len(segment_ids) == 0: if not segment_ids or len(segment_ids) == 0:
return return
db.session.commit() db.session.commit()


disable_segments_from_index_task.delay(real_deal_segment_ids, dataset.id, document.id) disable_segments_from_index_task.delay(real_deal_segment_ids, dataset.id, document.id)
else:
raise InvalidActionError()


@classmethod @classmethod
def create_child_chunk( def create_child_chunk(

+ 2
- 1
api/tasks/deal_dataset_vector_index_task.py Zobrazit soubor

import logging import logging
import time import time
from typing import Literal


import click import click
from celery import shared_task # type: ignore from celery import shared_task # type: ignore




@shared_task(queue="dataset") @shared_task(queue="dataset")
def deal_dataset_vector_index_task(dataset_id: str, action: str):
def deal_dataset_vector_index_task(dataset_id: str, action: Literal["remove", "add", "update"]):
""" """
Async deal dataset from index Async deal dataset from index
:param dataset_id: dataset_id :param dataset_id: dataset_id

+ 9
- 0
api/tests/integration_tests/vdb/qdrant/test_qdrant.py Zobrazit soubor

from core.rag.datasource.vdb.qdrant.qdrant_vector import QdrantConfig, QdrantVector from core.rag.datasource.vdb.qdrant.qdrant_vector import QdrantConfig, QdrantVector
from core.rag.models.document import Document
from tests.integration_tests.vdb.test_vector_store import ( from tests.integration_tests.vdb.test_vector_store import (
AbstractVectorTest, AbstractVectorTest,
setup_mock_redis, setup_mock_redis,
), ),
) )


def search_by_vector(self):
super().search_by_vector()
# only test for qdrant, may not work on other vector stores
hits_by_vector: list[Document] = self.vector.search_by_vector(
query_vector=self.example_embedding, score_threshold=1
)
assert len(hits_by_vector) == 0



def test_qdrant_vector(setup_mock_redis): def test_qdrant_vector(setup_mock_redis):
QdrantVectorTest().run_all_tests() QdrantVectorTest().run_all_tests()

+ 2
- 2
api/tests/test_containers_integration_tests/services/test_annotation_service.py Zobrazit soubor

# Verify annotation was deleted # Verify annotation was deleted
from extensions.ext_database import db from extensions.ext_database import db


deleted_annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
deleted_annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
assert deleted_annotation is None assert deleted_annotation is None


# Verify delete_annotation_index_task was called (when annotation setting exists) # Verify delete_annotation_index_task was called (when annotation setting exists)
AppAnnotationService.delete_app_annotation(app.id, annotation_id) AppAnnotationService.delete_app_annotation(app.id, annotation_id)


# Verify annotation was deleted # Verify annotation was deleted
deleted_annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
deleted_annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
assert deleted_annotation is None assert deleted_annotation is None


# Verify delete_annotation_index_task was called # Verify delete_annotation_index_task was called

+ 1
- 1
api/tests/test_containers_integration_tests/services/test_api_based_extension_service.py Zobrazit soubor

# Verify extension was deleted # Verify extension was deleted
from extensions.ext_database import db from extensions.ext_database import db


deleted_extension = db.session.query(APIBasedExtension).filter(APIBasedExtension.id == extension_id).first()
deleted_extension = db.session.query(APIBasedExtension).where(APIBasedExtension.id == extension_id).first()
assert deleted_extension is None assert deleted_extension is None


def test_save_extension_duplicate_name(self, db_session_with_containers, mock_external_service_dependencies): def test_save_extension_duplicate_name(self, db_session_with_containers, mock_external_service_dependencies):

+ 1785
- 0
api/tests/test_containers_integration_tests/services/test_feature_service.py
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 1
- 1
api/tests/test_containers_integration_tests/services/test_message_service.py Zobrazit soubor

# Verify feedback was deleted # Verify feedback was deleted
from extensions.ext_database import db from extensions.ext_database import db


deleted_feedback = db.session.query(MessageFeedback).filter(MessageFeedback.id == feedback.id).first()
deleted_feedback = db.session.query(MessageFeedback).where(MessageFeedback.id == feedback.id).first()
assert deleted_feedback is None assert deleted_feedback is None


def test_create_feedback_no_rating_when_not_exists( def test_create_feedback_no_rating_when_not_exists(

+ 1
- 1
api/tests/test_containers_integration_tests/services/test_model_load_balancing_service.py Zobrazit soubor



# Verify inherit config was created in database # Verify inherit config was created in database
inherit_configs = ( inherit_configs = (
db.session.query(LoadBalancingModelConfig).filter(LoadBalancingModelConfig.name == "__inherit__").all()
db.session.query(LoadBalancingModelConfig).where(LoadBalancingModelConfig.name == "__inherit__").all()
) )
assert len(inherit_configs) == 1 assert len(inherit_configs) == 1

+ 15
- 4
api/uv.lock Zobrazit soubor

{ name = "pgvector", specifier = "==0.2.5" }, { name = "pgvector", specifier = "==0.2.5" },
{ name = "pymilvus", specifier = "~=2.5.0" }, { name = "pymilvus", specifier = "~=2.5.0" },
{ name = "pymochow", specifier = "==1.3.1" }, { name = "pymochow", specifier = "==1.3.1" },
{ name = "pyobvector", specifier = "~=0.1.6" },
{ name = "pyobvector", specifier = "~=0.2.15" },
{ name = "qdrant-client", specifier = "==1.9.0" }, { name = "qdrant-client", specifier = "==1.9.0" },
{ name = "tablestore", specifier = "==6.2.0" }, { name = "tablestore", specifier = "==6.2.0" },
{ name = "tcvectordb", specifier = "~=1.6.4" }, { name = "tcvectordb", specifier = "~=1.6.4" },


[[package]] [[package]]
name = "pyobvector" name = "pyobvector"
version = "0.1.14"
version = "0.2.15"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiomysql" }, { name = "aiomysql" },
{ name = "numpy" }, { name = "numpy" },
{ name = "pydantic" },
{ name = "pymysql" }, { name = "pymysql" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "sqlglot" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/dc/59/7d762061808948dd6aad165a000b34e22163dc83fb5014184eeacc0fabe5/pyobvector-0.1.14.tar.gz", hash = "sha256:4f85cdd63064d040e94c0a96099a0cd5cda18ce625865382e89429f28422fc02", size = 26780, upload-time = "2024-11-20T11:46:18.017Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/7d/3f3aac6acf1fdd1782042d6eecd48efaa2ee355af0dbb61e93292d629391/pyobvector-0.2.15.tar.gz", hash = "sha256:5de258c1e952c88b385b5661e130c1cf8262c498c1f8a4a348a35962d379fce4", size = 39611, upload-time = "2025-08-18T02:49:26.683Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/68/ecb21b74c974e7be7f9034e205d08db62d614ff5c221581ae96d37ef853e/pyobvector-0.1.14-py3-none-any.whl", hash = "sha256:828e0bec49a177355b70c7a1270af3b0bf5239200ee0d096e4165b267eeff97c", size = 35526, upload-time = "2024-11-20T11:46:16.809Z" },
{ url = "https://files.pythonhosted.org/packages/5f/1f/a62754ba9b8a02c038d2a96cb641b71d3809f34d2ba4f921fecd7840d7fb/pyobvector-0.2.15-py3-none-any.whl", hash = "sha256:feeefe849ee5400e72a9a4d3844e425a58a99053dd02abe06884206923065ebb", size = 52680, upload-time = "2025-08-18T02:49:25.452Z" },
] ]


[[package]] [[package]]
{ url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
] ]


[[package]]
name = "sqlglot"
version = "26.33.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/9d/fcd59b4612d5ad1e2257c67c478107f073b19e1097d3bfde2fb517884416/sqlglot-26.33.0.tar.gz", hash = "sha256:2817278779fa51d6def43aa0d70690b93a25c83eb18ec97130fdaf707abc0d73", size = 5353340, upload-time = "2025-07-01T13:09:06.311Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/8d/f1d9cb5b18e06aa45689fbeaaea6ebab66d5f01d1e65029a8f7657c06be5/sqlglot-26.33.0-py3-none-any.whl", hash = "sha256:031cee20c0c796a83d26d079a47fdce667604df430598c7eabfa4e4dfd147033", size = 477610, upload-time = "2025-07-01T13:09:03.926Z" },
]

[[package]] [[package]]
name = "sseclient-py" name = "sseclient-py"
version = "1.8.0" version = "1.8.0"

+ 8
- 0
docker/.env.example Zobrazit soubor

# API workflow node execution repository implementation # API workflow node execution repository implementation
API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository


# Workflow log cleanup configuration
# Enable automatic cleanup of workflow run logs to manage database size
WORKFLOW_LOG_CLEANUP_ENABLED=false
# Number of days to retain workflow run logs (default: 30 days)
WORKFLOW_LOG_RETENTION_DAYS=30
# Batch size for workflow log cleanup operations (default: 100)
WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100

# HTTP request node in workflow configuration # HTTP request node in workflow configuration
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760 HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576 HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576

+ 3
- 0
docker/docker-compose.yaml Zobrazit soubor

CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY:-core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository} CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY:-core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository}
API_WORKFLOW_RUN_REPOSITORY: ${API_WORKFLOW_RUN_REPOSITORY:-repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository} API_WORKFLOW_RUN_REPOSITORY: ${API_WORKFLOW_RUN_REPOSITORY:-repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository}
API_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${API_WORKFLOW_NODE_EXECUTION_REPOSITORY:-repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository} API_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${API_WORKFLOW_NODE_EXECUTION_REPOSITORY:-repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository}
WORKFLOW_LOG_CLEANUP_ENABLED: ${WORKFLOW_LOG_CLEANUP_ENABLED:-false}
WORKFLOW_LOG_RETENTION_DAYS: ${WORKFLOW_LOG_RETENTION_DAYS:-30}
WORKFLOW_LOG_CLEANUP_BATCH_SIZE: ${WORKFLOW_LOG_CLEANUP_BATCH_SIZE:-100}
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760} HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760}
HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576} HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576}
HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True} HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True}

+ 47
- 4
web/app/account/account-page/AvatarWithEdit.tsx Zobrazit soubor

import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { RiPencilLine } from '@remixicon/react'
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
import { updateUserProfile } from '@/service/common' import { updateUserProfile } from '@/service/common'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput' import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>() const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false) const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false)
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [isShowDeleteConfirm, setIsShowDeleteConfirm] = useState(false)
const [hoverArea, setHoverArea] = useState<string>('left')


const handleImageInput: OnImageInput = useCallback(async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => { const handleImageInput: OnImageInput = useCallback(async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
setInputImageInfo( setInputImageInfo(
} }
}, [notify, onSave, t]) }, [notify, onSave, t])


const handleDeleteAvatar = useCallback(async () => {
try {
await updateUserProfile({ url: 'account/avatar', body: { avatar: '' } })
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
setIsShowDeleteConfirm(false)
onSave?.()
}
catch (e) {
notify({ type: 'error', message: (e as Error).message })
}
}, [notify, onSave, t])

const { handleLocalFileUpload } = useLocalFileUploader({ const { handleLocalFileUpload } = useLocalFileUploader({
limit: 3, limit: 3,
disabled: false, disabled: false,
<div className="group relative"> <div className="group relative">
<Avatar {...props} /> <Avatar {...props} />
<div <div
onClick={() => { setIsShowAvatarPicker(true) }}
className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100" className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
onClick={() => hoverArea === 'right' ? setIsShowDeleteConfirm(true) : setIsShowAvatarPicker(true)}
onMouseMove={(e) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const isRight = x > rect.width / 2
setHoverArea(isRight ? 'right' : 'left')
}}
> >
<span className="text-xs text-white">
{hoverArea === 'right' ? <span className="text-xs text-white">
<RiDeleteBin5Line />
</span> : <span className="text-xs text-white">
<RiPencilLine /> <RiPencilLine />
</span>
</span>}

</div> </div>
</div> </div>
</div> </div>
</Button> </Button>
</div> </div>
</Modal> </Modal>

<Modal
closable
className="!w-[362px] !p-6"
isShow={isShowDeleteConfirm}
onClose={() => setIsShowDeleteConfirm(false)}
>
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('common.avatar.deleteTitle')}</div>
<p className="mb-8 text-text-secondary">{t('common.avatar.deleteDescription')}</p>

<div className="flex w-full items-center justify-center gap-2">
<Button className="w-full" onClick={() => setIsShowDeleteConfirm(false)}>
{t('common.operation.cancel')}
</Button>

<Button variant="warning" className="w-full" onClick={handleDeleteAvatar}>
{t('common.operation.delete')}
</Button>
</div>
</Modal>
</> </>
) )
} }

+ 10
- 6
web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx Zobrazit soubor

import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import { getNewVar, getVars } from '@/utils/var' import { getNewVar, getVars } from '@/utils/var'
import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn' import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn'
import type { AutomaticRes } from '@/service/debug'
import type { GenRes } from '@/service/debug'
import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res' import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
import PromptEditor from '@/app/components/base/prompt-editor' import PromptEditor from '@/app/components/base/prompt-editor'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'


const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const { const {
appId,
modelConfig, modelConfig,
dataSets, dataSets,
setModelConfig, setModelConfig,
} }


const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false) const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = (res: AutomaticRes) => {
const handleAutomaticRes = (res: GenRes) => {
// put eventEmitter in first place to prevent overwrite the configs.prompt_variables.But another problem is that prompt won't hight the prompt_variables. // put eventEmitter in first place to prevent overwrite the configs.prompt_variables.But another problem is that prompt won't hight the prompt_variables.
eventEmitter?.emit({ eventEmitter?.emit({
type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER, type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
payload: res.prompt,
payload: res.modified,
} as any) } as any)
const newModelConfig = produce(modelConfig, (draft) => { const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_template = res.prompt
draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true }))
draft.configs.prompt_template = res.modified
draft.configs.prompt_variables = (res.variables || []).map(key => ({ key, name: key, type: 'string', required: true }))
}) })
setModelConfig(newModelConfig) setModelConfig(newModelConfig)
setPrevPromptConfig(modelConfig.configs) setPrevPromptConfig(modelConfig.configs)


if (mode !== AppType.completion) { if (mode !== AppType.completion) {
setIntroduction(res.opening_statement)
setIntroduction(res.opening_statement || '')
const newFeatures = produce(features, (draft) => { const newFeatures = produce(features, (draft) => {
draft.opening = { draft.opening = {
...draft.opening, ...draft.opening,


{showAutomatic && ( {showAutomatic && (
<GetAutomaticResModal <GetAutomaticResModal
flowId={appId}
mode={mode as AppType} mode={mode as AppType}
isShow={showAutomatic} isShow={showAutomatic}
onClose={showAutomaticFalse} onClose={showAutomaticFalse}
onFinished={handleAutomaticRes} onFinished={handleAutomaticRes}
currentPrompt={promptTemplate}
isBasicMode
/> />
)} )}
</div> </div>

+ 156
- 127
web/app/components/app/configuration/config/automatic/get-automatic-res.tsx Zobrazit soubor

import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { useBoolean, useSessionStorageState } from 'ahooks'
import { import {
RiDatabase2Line, RiDatabase2Line,
RiFileExcel2Line, RiFileExcel2Line,
RiTranslate, RiTranslate,
RiUser2Line, RiUser2Line,
} from '@remixicon/react' } from '@remixicon/react'
import cn from 'classnames'
import s from './style.module.css' import s from './style.module.css'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { generateRule } from '@/service/debug'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import { generateBasicAppFistTimeRule, generateRule } from '@/service/debug'
import type { CompletionParams, Model } from '@/types/app' import type { CompletionParams, Model } from '@/types/app'
import { AppType } from '@/types/app'
import ConfigVar from '@/app/components/app/configuration/config-var'
import GroupName from '@/app/components/app/configuration/base/group-name'
import type { AppType } from '@/types/app'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { LoveMessage } from '@/app/components/base/icons/src/vender/features'


// type // type
import type { AutomaticRes } from '@/service/debug'
import type { GenRes } from '@/service/debug'
import { Generator } from '@/app/components/base/icons/src/vender/other' import { Generator } from '@/app/components/base/icons/src/vender/other'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'


import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import type { ModelModeType } from '@/types/app' import type { ModelModeType } from '@/types/app'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
import InstructionEditorInBasic from './instruction-editor'
import { GeneratorType } from './types'
import Result from './result'
import useGenData from './use-gen-data'
import IdeaOutput from './idea-output'
import ResPlaceholder from './res-placeholder'
import { useGenerateRuleTemplate } from '@/service/use-apps'


const i18nPrefix = 'appDebug.generate'
export type IGetAutomaticResProps = { export type IGetAutomaticResProps = {
mode: AppType mode: AppType
isShow: boolean isShow: boolean
onClose: () => void onClose: () => void
onFinished: (res: AutomaticRes) => void
isInLLMNode?: boolean
onFinished: (res: GenRes) => void
flowId?: string
nodeId?: string
currentPrompt?: string
isBasicMode?: boolean
} }


const TryLabel: FC<{ const TryLabel: FC<{
mode, mode,
isShow, isShow,
onClose, onClose,
isInLLMNode,
flowId,
nodeId,
currentPrompt,
isBasicMode,
onFinished, onFinished,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
}, },
] ]


const [instruction, setInstruction] = useState<string>('')
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}`}`)
const instruction = instructionFromSessionStorage || ''
const [ideaOutput, setIdeaOutput] = useState<string>('')

const [editorKey, setEditorKey] = useState(`${flowId}-0`)
const handleChooseTemplate = useCallback((key: string) => { const handleChooseTemplate = useCallback((key: string) => {
return () => { return () => {
const template = t(`appDebug.generate.template.${key}.instruction`) const template = t(`appDebug.generate.template.${key}.instruction`)
setInstruction(template) setInstruction(template)
setEditorKey(`${flowId}-${Date.now()}`)
} }
}, [t]) }, [t])

const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.prompt, isBasicMode)
useEffect(() => {
if (!instruction && instructionTemplate)
setInstruction(instructionTemplate.data)

setEditorKey(`${flowId}-${Date.now()}`)
}, [instructionTemplate])

const isValid = () => { const isValid = () => {
if (instruction.trim() === '') { if (instruction.trim() === '') {
Toast.notify({ Toast.notify({
return true return true
} }
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false) const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
const [res, setRes] = useState<AutomaticRes | null>(null)
const storageKey = `${flowId}${isBasicMode ? '' : `-${nodeId}`}`
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
storageKey,
})


useEffect(() => { useEffect(() => {
if (defaultModel) { if (defaultModel) {
</div> </div>
) )


const renderNoData = (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='h-14 w-14 text-text-tertiary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.generate.noDataLine1')}</div>
<div>{t('appDebug.generate.noDataLine2')}</div>
</div>
</div>
)

const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => { const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => {
const newModel = { const newModel = {
...model, ...model,
return return
setLoadingTrue() setLoadingTrue()
try { try {
const { error, ...res } = await generateRule({
instruction,
model_config: model,
no_variable: !!isInLLMNode,
})
setRes(res)
if (error) {
Toast.notify({
type: 'error',
message: error,
let apiRes: GenRes
let hasError = false
if (isBasicMode || !currentPrompt) {
const { error, ...res } = await generateBasicAppFistTimeRule({
instruction,
model_config: model,
no_variable: false,
})
apiRes = {
...res,
modified: res.prompt,
} as GenRes
if (error) {
hasError = true
Toast.notify({
type: 'error',
message: error,
})
}
}
else {
const { error, ...res } = await generateRule({
flow_id: flowId,
node_id: nodeId,
current: currentPrompt,
instruction,
ideal_output: ideaOutput,
model_config: model,
}) })
apiRes = res
if (error) {
hasError = true
Toast.notify({
type: 'error',
message: error,
})
}
} }
if (!hasError)
addVersion(apiRes)
} }
finally { finally {
setLoadingFalse() setLoadingFalse()
} }
} }


const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
const [isShowConfirmOverwrite, {
setTrue: showConfirmOverwrite,
setFalse: hideShowConfirmOverwrite,
}] = useBoolean(false)


const isShowAutoPromptResPlaceholder = () => { const isShowAutoPromptResPlaceholder = () => {
return !isLoading && !res
return !isLoading && !current
} }


return ( return (
isShow={isShow} isShow={isShow}
onClose={onClose} onClose={onClose}
className='min-w-[1140px] !p-0' className='min-w-[1140px] !p-0'
closable
> >
<div className='flex h-[680px] flex-wrap'> <div className='flex h-[680px] flex-wrap'>
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'> <div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'>
<div className='mb-8'>
<div className='mb-5'>
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.generate.title')}</div> <div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.generate.title')}</div>
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div> <div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div>
</div> </div>
<div className='mb-8'>
<div>
<ModelParameterModal <ModelParameterModal
popupClassName='!w-[520px]' popupClassName='!w-[520px]'
portalToFollowElemContentClassName='z-[1000]' portalToFollowElemContentClassName='z-[1000]'
hideDebugWithMultipleModel hideDebugWithMultipleModel
/> />
</div> </div>
<div >
<div className='flex items-center'>
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
<div className='h-px grow' style={{
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
}}></div>
{isBasicMode && (
<div className='mt-4'>
<div className='flex items-center'>
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
<div className='h-px grow' style={{
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
}}></div>
</div>
<div className='flex flex-wrap'>
{tryList.map(item => (
<TryLabel
key={item.key}
Icon={item.icon}
text={t(`appDebug.generate.template.${item.key}.name`)}
onClick={handleChooseTemplate(item.key)}
/>
))}
</div>
</div> </div>
<div className='flex flex-wrap'>
{tryList.map(item => (
<TryLabel
key={item.key}
Icon={item.icon}
text={t(`appDebug.generate.template.${item.key}.name`)}
onClick={handleChooseTemplate(item.key)}
/>
))}
</div>
</div>
)}

{/* inputs */} {/* inputs */}
<div className='mt-6'>
<div className='text-[0px]'>
<div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.generate.instruction')}</div>
<Textarea
className="h-[200px] resize-none"
placeholder={t('appDebug.generate.instructionPlaceHolder') as string}
value={instruction}
onChange={e => setInstruction(e.target.value)} />
<div className='mt-4'>
<div>
<div className='system-sm-semibold-uppercase mb-1.5 text-text-secondary'>{t('appDebug.generate.instruction')}</div>
{isBasicMode ? (
<InstructionEditorInBasic
editorKey={editorKey}
generatorType={GeneratorType.prompt}
value={instruction}
onChange={setInstruction}
availableVars={[]}
availableNodes={[]}
isShowCurrentBlock={!!currentPrompt}
isShowLastRunBlock={false}
/>
) : (
<InstructionEditorInWorkflow
editorKey={editorKey}
generatorType={GeneratorType.prompt}
value={instruction}
onChange={setInstruction}
nodeId={nodeId || ''}
isShowCurrentBlock={!!currentPrompt}
/>
)}
</div> </div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>


<div className='mt-5 flex justify-end'>
<div className='mt-7 flex justify-end space-x-2'>
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`)}</Button>
<Button <Button
className='flex space-x-1' className='flex space-x-1'
variant='primary' variant='primary'
onClick={onGenerate} onClick={onGenerate}
disabled={isLoading} disabled={isLoading}
> >
<Generator className='h-4 w-4 text-white' />
<span className='text-xs font-semibold text-white'>{t('appDebug.generate.generate')}</span>
<Generator className='h-4 w-4' />
<span className='text-xs font-semibold'>{t('appDebug.generate.generate')}</span>
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>


{(!isLoading && res) && (
<div className='h-full w-0 grow p-6 pb-0'>
<div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
<ConfigPrompt
mode={mode}
promptTemplate={res?.prompt || ''}
promptVariables={[]}
readonly
noTitle={isInLLMNode}
gradientBorder
editorHeight={isInLLMNode ? 524 : 0}
noResize={isInLLMNode}
/>
{!isInLLMNode && (
<>
{(res?.variables?.length && res?.variables?.length > 0)
? (
<ConfigVar
promptVariables={res?.variables.map(key => ({ key, name: key, type: 'string', required: true })) || []}
readonly
/>
)
: ''}

{(mode !== AppType.completion && res?.opening_statement) && (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.groupChat.title')} />
<div
className='mb-1 rounded-xl border-l-[0.5px] border-t-[0.5px] border-effects-highlight bg-background-section-burn p-3'
>
<div className='mb-2 flex items-center gap-2'>
<div className='shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-light-blue-light-500 p-1 shadow-xs'>
<LoveMessage className='h-4 w-4 text-text-primary-on-surface' />
</div>
<div className='system-sm-semibold flex grow items-center text-text-secondary'>
{t('appDebug.feature.conversationOpener.title')}
</div>
</div>
<div className='system-xs-regular min-h-8 text-text-tertiary'>{res.opening_statement}</div>
</div>
</div>
)}
</>
)}
</div>

<div className='flex justify-end bg-background-default py-4'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='ml-2' onClick={() => {
setShowConfirmOverwrite(true)
}}>{t('appDebug.generate.apply')}</Button>
</div>
{(!isLoading && current) && (
<div className='h-full w-0 grow bg-background-default-subtle p-6 pb-0'>
<Result
current={current!}
isBasicMode={isBasicMode}
nodeId={nodeId!}
currentVersionIndex={currentVersionIndex || 0}
setCurrentVersionIndex={setCurrentVersionIndex}
versions={versions || []}
onApply={showConfirmOverwrite}
generatorType={GeneratorType.prompt}
/>
</div> </div>
)} )}
{isLoading && renderLoading} {isLoading && renderLoading}
{isShowAutoPromptResPlaceholder() && renderNoData}
{showConfirmOverwrite && (
{isShowAutoPromptResPlaceholder() && <ResPlaceholder />}
{isShowConfirmOverwrite && (
<Confirm <Confirm
title={t('appDebug.generate.overwriteTitle')} title={t('appDebug.generate.overwriteTitle')}
content={t('appDebug.generate.overwriteMessage')} content={t('appDebug.generate.overwriteMessage')}
isShow={showConfirmOverwrite}
isShow
onConfirm={() => { onConfirm={() => {
setShowConfirmOverwrite(false)
onFinished(res!)
hideShowConfirmOverwrite()
onFinished(current!)
}} }}
onCancel={() => setShowConfirmOverwrite(false)}
onCancel={hideShowConfirmOverwrite}
/> />
)} )}
</div> </div>

+ 48
- 0
web/app/components/app/configuration/config/automatic/idea-output.tsx Zobrazit soubor

'use client'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import { useBoolean } from 'ahooks'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import Textarea from '@/app/components/base/textarea'
import { useTranslation } from 'react-i18next'

const i18nPrefix = 'appDebug.generate'

type Props = {
value: string
onChange: (value: string) => void
}

const IdeaOutput: FC<Props> = ({
value,
onChange,
}) => {
const { t } = useTranslation()

const [isFoldIdeaOutput, {
toggle: toggleFoldIdeaOutput,
}] = useBoolean(true)

return (
<div className='mt-4 text-[0px]'>
<div
className='mb-1.5 flex cursor-pointer items-center text-sm font-medium leading-5 text-text-primary'
onClick={toggleFoldIdeaOutput}
>
<div className='system-sm-semibold-uppercase mr-1 text-text-secondary'>{t(`${i18nPrefix}.idealOutput`)}</div>
<div className='system-xs-regular text-text-tertiary'>({t(`${i18nPrefix}.optional`)})</div>
<ArrowDownRoundFill className={cn('size text-text-quaternary', isFoldIdeaOutput && 'relative top-[1px] rotate-[-90deg]')} />
</div>
{!isFoldIdeaOutput && (
<Textarea
className="h-[80px]"
placeholder={t(`${i18nPrefix}.idealOutputPlaceholder`)}
value={value}
onChange={e => onChange(e.target.value)}
/>
)}
</div>
)
}
export default React.memo(IdeaOutput)

+ 58
- 0
web/app/components/app/configuration/config/automatic/instruction-editor-in-workflow.tsx Zobrazit soubor

'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import type { GeneratorType } from './types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import InstructionEditor from './instruction-editor'
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
import { useWorkflowStore } from '@/app/components/workflow/store'

type Props = {
nodeId: string
value: string
editorKey: string
onChange: (text: string) => void
generatorType: GeneratorType
isShowCurrentBlock: boolean
}

const InstructionEditorInWorkflow: FC<Props> = ({
nodeId,
value,
editorKey,
onChange,
generatorType,
isShowCurrentBlock,
}) => {
const workflowStore = useWorkflowStore()
const filterVar = useCallback((payload: Var, selector: ValueSelector) => {
const { nodesWithInspectVars } = workflowStore.getState()
const nodeId = selector?.[0]
return !!nodesWithInspectVars.find(node => node.nodeId === nodeId) && payload.type !== VarType.file && payload.type !== VarType.arrayFile
}, [workflowStore])
const {
availableVars,
availableNodes,
} = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar,
})
const getVarType = useWorkflowVariableType()

return (
<InstructionEditor
value={value}
onChange={onChange}
editorKey={editorKey}
generatorType={generatorType}
availableVars={availableVars}
availableNodes={availableNodes}
getVarType={getVarType}
isShowCurrentBlock={isShowCurrentBlock}
isShowLastRunBlock
/>
)
}
export default React.memo(InstructionEditorInWorkflow)

+ 117
- 0
web/app/components/app/configuration/config/automatic/instruction-editor.tsx Zobrazit soubor

'use client'
import type { FC } from 'react'
import React from 'react'
import PromptEditor from '@/app/components/base/prompt-editor'
import type { GeneratorType } from './types'
import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
import { useEventEmitterContextContext } from '@/context/event-emitter'

type Props = {
editorKey: string
value: string
onChange: (text: string) => void
generatorType: GeneratorType
availableVars: NodeOutPutVar[]
availableNodes: Node[]
getVarType?: (params: {
nodeId: string,
valueSelector: ValueSelector,
}) => Type
isShowCurrentBlock: boolean
isShowLastRunBlock: boolean
}

const i18nPrefix = 'appDebug.generate'

const InstructionEditor: FC<Props> = ({
editorKey,
generatorType,
value,
onChange,
availableVars,
availableNodes,
getVarType = () => Type.string,
isShowCurrentBlock,
isShowLastRunBlock,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()

const isCode = generatorType === 'code'
const placeholder = isCode ? <div className='system-sm-regular whitespace-break-spaces !leading-6 text-text-placeholder'>
{t(`${i18nPrefix}.codeGenInstructionPlaceHolderLine`)}
</div> : (
<div className='system-sm-regular text-text-placeholder'>
<div className='leading-6'>{t(`${i18nPrefix}.instructionPlaceHolderTitle`)}</div>
<div className='mt-2'>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine1`)}</div>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine2`)}</div>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine3`)}</div>
</div>
</div>
)

const handleInsertVariable = () => {
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId: editorKey } as any)
}

return (
<div className='relative'>
<PromptEditor
wrapperClassName='border !border-components-input-bg-normal bg-components-input-bg-normal hover:!border-components-input-bg-hover rounded-[10px] px-4 pt-3'
key={editorKey}
instanceId={editorKey}
placeholder={placeholder}
placeholderClassName='px-4 pt-3'
className={cn('min-h-[240px] pb-8')}
value={value}
workflowVariableBlock={{
show: true,
variables: availableVars,
getVarType,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
currentBlock={{
show: isShowCurrentBlock,
generatorType,
}}
errorMessageBlock={{
show: isCode,
}}
lastRunBlock={{
show: isShowLastRunBlock,
}}
onChange={onChange}
editable
isSupportFileVar={false}
/>
<div className='system-xs-regular absolute bottom-0 left-4 flex h-8 items-center space-x-0.5 text-components-input-text-placeholder'>
<span>{t('appDebug.generate.press')}</span>
<span className='system-kbd flex h-4 w-3.5 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray text-text-placeholder'>/</span>
<span>{t('appDebug.generate.to')}</span>
<span onClick={handleInsertVariable} className='!ml-1 cursor-pointer hover:border-b hover:border-dotted hover:border-text-tertiary hover:text-text-tertiary'>{t('appDebug.generate.insertContext')}</span>
</div>
</div>
)
}
export default React.memo(InstructionEditor)

+ 55
- 0
web/app/components/app/configuration/config/automatic/prompt-res-in-workflow.tsx Zobrazit soubor

'use client'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import type { FC } from 'react'
import React from 'react'
import PromptRes from './prompt-res'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'

type Props = {
value: string
nodeId: string
}

const PromptResInWorkflow: FC<Props> = ({
value,
nodeId,
}) => {
const { t } = useTranslation()
const {
availableVars,
availableNodes,
} = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: _payload => true,
})
return (
<PromptRes
value={value}
workflowVariableBlock={{
show: true,
variables: availableVars || [],
getVarType: () => Type.string,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
>
</PromptRes>
)
}
export default React.memo(PromptResInWorkflow)

+ 31
- 0
web/app/components/app/configuration/config/automatic/prompt-res.tsx Zobrazit soubor

'use client'
import PromptEditor from '@/app/components/base/prompt-editor'
import type { WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types'
import type { FC } from 'react'
import React, { useEffect } from 'react'

type Props = {
value: string
workflowVariableBlock: WorkflowVariableBlockType
}

const keyIdPrefix = 'prompt-res-editor'
const PromptRes: FC<Props> = ({
value,
workflowVariableBlock,
}) => {
const [editorKey, setEditorKey] = React.useState<string>('keyIdPrefix-0')
useEffect(() => {
setEditorKey(`${keyIdPrefix}-${Date.now()}`)
}, [value])
return (
<PromptEditor
key={editorKey}
value={value}
editable={false}
className='h-full bg-transparent pt-0'
workflowVariableBlock={workflowVariableBlock}
/>
)
}
export default React.memo(PromptRes)

+ 54
- 0
web/app/components/app/configuration/config/automatic/prompt-toast.tsx Zobrazit soubor

import { RiArrowDownSLine, RiSparklingFill } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import React from 'react'
import cn from '@/utils/classnames'
import { Markdown } from '@/app/components/base/markdown'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'

type Props = {
message: string
className?: string
}
const PromptToast = ({
message,
className,
}: Props) => {
const { t } = useTranslation()
const [isFold, {
toggle: toggleFold,
}] = useBoolean(false)
// const message = `
// list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1
// # h1
// **strong text** ~~strikethrough~~

// * list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1
// * list2

// xxxx

// ## h2
// \`\`\`python
// print('Hello, World!')
// \`\`\`
// `
return (
<div className={cn('rounded-xl border-[0.5px] border-components-panel-border bg-background-section-burn pl-4 shadow-xs', className)}>
<div className='my-3 flex h-4 items-center justify-between pr-3'>
<div className='flex items-center space-x-1'>
<RiSparklingFill className='size-3.5 text-components-input-border-active-prompt-1' />
<span className={cn(s.optimizationNoteText, 'system-xs-semibold-uppercase')}>{t('appDebug.generate.optimizationNote')}</span>
</div>
<RiArrowDownSLine className={cn('size-4 cursor-pointer text-text-tertiary', isFold && 'rotate-[-90deg]')} onClick={toggleFold} />
</div>
{!isFold && (
<div className='pb-4 pr-4'>
<Markdown className="!text-sm" content={message} />
</div>
)}
</div>
)
}

export default PromptToast

+ 18
- 0
web/app/components/app/configuration/config/automatic/res-placeholder.tsx Zobrazit soubor

'use client'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'

const ResPlaceholder: FC = () => {
const { t } = useTranslation()
return (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='size-8 text-text-quaternary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.generate.newNoDataLine1')}</div>
</div>
</div>
)
}
export default React.memo(ResPlaceholder)

+ 97
- 0
web/app/components/app/configuration/config/automatic/result.tsx Zobrazit soubor

'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { GeneratorType } from './types'
import PromptToast from './prompt-toast'
import Button from '@/app/components/base/button'
import VersionSelector from './version-selector'
import type { GenRes } from '@/service/debug'
import { RiClipboardLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import Toast from '@/app/components/base/toast'
import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor'
import PromptRes from './prompt-res'
import PromptResInWorkflow from './prompt-res-in-workflow'

type Props = {
isBasicMode?: boolean
nodeId?: string
current: GenRes
currentVersionIndex: number
setCurrentVersionIndex: (index: number) => void
versions: GenRes[]
onApply: () => void
generatorType: GeneratorType
}

const Result: FC<Props> = ({
isBasicMode,
nodeId,
current,
currentVersionIndex,
setCurrentVersionIndex,
versions,
onApply,
generatorType,
}) => {
const { t } = useTranslation()
const isGeneratorPrompt = generatorType === GeneratorType.prompt

return (
<div className='flex h-full flex-col'>
<div className='mb-3 flex shrink-0 items-center justify-between'>
<div>
<div className='shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
<VersionSelector
versionLen={versions.length}
value={currentVersionIndex}
onChange={setCurrentVersionIndex}
/>
</div>
<div className='flex items-center space-x-2'>
<Button className='px-2' onClick={() => {
copy(current.modified)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<RiClipboardLine className='h-4 w-4 text-text-secondary' />
</Button>
<Button variant='primary' onClick={onApply}>
{t('appDebug.generate.apply')}
</Button>
</div>
</div>
<div className='flex grow flex-col overflow-y-auto'>
{
current?.message && (
<PromptToast message={current.message} className='mb-3 shrink-0' />
)
}
<div className='grow pb-6'>
{isGeneratorPrompt ? (
isBasicMode ? (
<PromptRes
value={current?.modified}
workflowVariableBlock={{
show: false,
}}
/>
) : (<PromptResInWorkflow
value={current?.modified || ''}
nodeId={nodeId!}
/>)
) : (
<CodeEditor
editorWrapperClassName='h-full'
className='bg-transparent pt-0'
value={current?.modified}
readOnly
hideTopMenu
/>
)}
</div>
</div>
</div>
)
}
export default React.memo(Result)

+ 7
- 1
web/app/components/app/configuration/config/automatic/style.module.css Zobrazit soubor

-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
text-fill-color: transparent;
}

.optimizationNoteText {
background: linear-gradient(263deg, rgba(21, 90, 239, 0.95) -20.92%, rgba(11, 165, 236, 0.95) 87.04%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }

+ 4
- 0
web/app/components/app/configuration/config/automatic/types.ts Zobrazit soubor

export enum GeneratorType {
prompt = 'prompt',
code = 'code',
}

+ 36
- 0
web/app/components/app/configuration/config/automatic/use-gen-data.ts Zobrazit soubor

import type { GenRes } from '@/service/debug'
import { useSessionStorageState } from 'ahooks'
import { useCallback } from 'react'

type Params = {
storageKey: string
}
const keyPrefix = 'gen-data-'
const useGenData = ({ storageKey }: Params) => {
const [versions, setVersions] = useSessionStorageState<GenRes[]>(`${keyPrefix}${storageKey}-versions`, {
defaultValue: [],
})

const [currentVersionIndex, setCurrentVersionIndex] = useSessionStorageState<number>(`${keyPrefix}${storageKey}-version-index`, {
defaultValue: 0,
})

const current = versions?.[currentVersionIndex || 0]

const addVersion = useCallback((version: GenRes) => {
setCurrentVersionIndex(() => versions?.length || 0)
setVersions((prev) => {
return [...prev!, version]
})
}, [setVersions, setCurrentVersionIndex, versions?.length])

return {
versions,
addVersion,
currentVersionIndex,
setCurrentVersionIndex,
current,
}
}

export default useGenData

+ 103
- 0
web/app/components/app/configuration/config/automatic/version-selector.tsx Zobrazit soubor

import React, { useCallback } from 'react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { useBoolean } from 'ahooks'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'

type Option = {
label: string
value: number
}

type VersionSelectorProps = {
versionLen: number;
value: number;
onChange: (index: number) => void;
}

const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, onChange }) => {
const { t } = useTranslation()
const [isOpen, {
setFalse: handleOpenFalse,
toggle: handleOpenToggle,
set: handleOpenSet,
}] = useBoolean(false)

const moreThanOneVersion = versionLen > 1
const handleOpen = useCallback((value: boolean) => {
if (moreThanOneVersion)
handleOpenSet(value)
}, [moreThanOneVersion, handleOpenToggle])
const handleToggle = useCallback(() => {
if (moreThanOneVersion)
handleOpenToggle()
}, [moreThanOneVersion, handleOpenToggle])

const versions = Array.from({ length: versionLen }, (_, index) => ({
label: `${t('appDebug.generate.version')} ${index + 1}${index === versionLen - 1 ? ` · ${t('appDebug.generate.latest')}` : ''}`,
value: index,
}))

const isLatest = value === versionLen - 1

return (
<PortalToFollowElem
placement={'bottom-start'}
offset={{
mainAxis: 4,
crossAxis: -12,
}}
open={isOpen}
onOpenChange={handleOpen}
>
<PortalToFollowElemTrigger
onClick={handleToggle}
asChild
>

<div className={cn('system-xs-medium flex items-center text-text-tertiary', isOpen && 'text-text-secondary', moreThanOneVersion && 'cursor-pointer')}>
<div>{t('appDebug.generate.version')} {value + 1}{isLatest && ` · ${t('appDebug.generate.latest')}`}</div>
{moreThanOneVersion && <RiArrowDownSLine className='size-3 ' />}
</div>
</PortalToFollowElemTrigger >
<PortalToFollowElemContent className={cn(
'z-[99]',
)}>
<div
className={cn(
'w-[208px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
)}
>
<div className={cn('system-xs-medium-uppercase flex h-[22px] items-center px-3 pl-3 text-text-tertiary')}>
{t('appDebug.generate.versions')}
</div>
{
versions.map(option => (
<div
key={option.value}
className={cn(
'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
)}
title={option.label}
onClick={() => {
onChange(option.value)
handleOpenFalse()
}}
>
<div className='mr-1 grow truncate px-1 pl-1'>
{option.label}
</div>
{
value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
}
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}

export default VersionSelector

+ 90
- 84
web/app/components/app/configuration/config/code-generator/get-code-generator-res.tsx Zobrazit soubor

import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useEffect } from 'react'
import cn from 'classnames'
import React, { useCallback, useEffect, useState } from 'react'
import useBoolean from 'ahooks/lib/useBoolean' import useBoolean from 'ahooks/lib/useBoolean'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ConfigPrompt from '../../config-prompt'
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index' import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
import { generateRuleCode } from '@/service/debug'
import type { CodeGenRes } from '@/service/debug'
import { generateRule } from '@/service/debug'
import type { GenRes } from '@/service/debug'
import type { ModelModeType } from '@/types/app' import type { ModelModeType } from '@/types/app'
import type { AppType, CompletionParams, Model } from '@/types/app' import type { AppType, CompletionParams, Model } from '@/types/app'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { Generator } from '@/app/components/base/icons/src/vender/other' import { Generator } from '@/app/components/base/icons/src/vender/other'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import IdeaOutput from '../automatic/idea-output'
import { GeneratorType } from '../automatic/types'
import InstructionEditor from '../automatic/instruction-editor-in-workflow'
import useGenData from '../automatic/use-gen-data'
import Result from '../automatic/result'
import ResPlaceholder from '../automatic/res-placeholder'
import { useGenerateRuleTemplate } from '@/service/use-apps'
import { useSessionStorageState } from 'ahooks'
import s from '../automatic/style.module.css'


const i18nPrefix = 'appDebug.generate'
export type IGetCodeGeneratorResProps = { export type IGetCodeGeneratorResProps = {
flowId: string
nodeId: string
currentCode?: string
mode: AppType mode: AppType
isShow: boolean isShow: boolean
codeLanguages: CodeLanguage codeLanguages: CodeLanguage
onClose: () => void onClose: () => void
onFinished: (res: CodeGenRes) => void
onFinished: (res: GenRes) => void
} }


export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
{ {
flowId,
nodeId,
currentCode,
mode, mode,
isShow, isShow,
codeLanguages, codeLanguages,
const { const {
defaultModel, defaultModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration) } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
const [instruction, setInstruction] = React.useState<string>('')
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}-${nodeId}`)
const instruction = instructionFromSessionStorage || ''

const [ideaOutput, setIdeaOutput] = useState<string>('')

const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false) const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
const [res, setRes] = React.useState<CodeGenRes | null>(null)
const storageKey = `${flowId}-${nodeId}`
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
storageKey,
})
const [editorKey, setEditorKey] = useState(`${flowId}-0`)
const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.code)
useEffect(() => {
if (!instruction && instructionTemplate)
setInstruction(instructionTemplate.data)

setEditorKey(`${flowId}-${Date.now()}`)
}, [instructionTemplate])

const isValid = () => { const isValid = () => {
if (instruction.trim() === '') { if (instruction.trim() === '') {
Toast.notify({ Toast.notify({
localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
}, [model, setModel]) }, [model, setModel])


const isInLLMNode = true
const onGenerate = async () => { const onGenerate = async () => {
if (!isValid()) if (!isValid())
return return
return return
setLoadingTrue() setLoadingTrue()
try { try {
const { error, ...res } = await generateRuleCode({
const { error, ...res } = await generateRule({
flow_id: flowId,
node_id: nodeId,
current: currentCode,
instruction, instruction,
model_config: model, model_config: model,
no_variable: !!isInLLMNode,
code_language: languageMap[codeLanguages] || 'javascript',
ideal_output: ideaOutput,
language: languageMap[codeLanguages] || 'javascript',
}) })
setRes(res)
if((res as any).code) // not current or current is the same as the template would return a code field
res.modified = (res as any).code

if (error) { if (error) {
Toast.notify({ Toast.notify({
type: 'error', type: 'error',
message: error, message: error,
}) })
} }
else {
addVersion(res)
}
} }
finally { finally {
setLoadingFalse() setLoadingFalse()
} }
} }
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)

const [isShowConfirmOverwrite, {
setTrue: showConfirmOverwrite,
setFalse: hideShowConfirmOverwrite,
}] = useBoolean(false)


useEffect(() => { useEffect(() => {
if (defaultModel) { if (defaultModel) {
<div className='text-[13px] text-text-tertiary'>{t('appDebug.codegen.loading')}</div> <div className='text-[13px] text-text-tertiary'>{t('appDebug.codegen.loading')}</div>
</div> </div>
) )
const renderNoData = (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='h-14 w-14 text-text-tertiary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.codegen.noDataLine1')}</div>
<div>{t('appDebug.codegen.noDataLine2')}</div>
</div>
</div>
)


return ( return (
<Modal <Modal
isShow={isShow} isShow={isShow}
onClose={onClose} onClose={onClose}
className='min-w-[1140px] !p-0' className='min-w-[1140px] !p-0'
closable
> >
<div className='relative flex h-[680px] flex-wrap'> <div className='relative flex h-[680px] flex-wrap'>
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-8'>
<div className='mb-8'>
<div className={'text-lg font-bold leading-[28px] text-text-primary'}>{t('appDebug.codegen.title')}</div>
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'>
<div className='mb-5'>
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.codegen.title')}</div>
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.codegen.description')}</div> <div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.codegen.description')}</div>
</div> </div>
<div className='mb-8'>
<div className='mb-4'>
<ModelParameterModal <ModelParameterModal
popupClassName='!w-[520px]' popupClassName='!w-[520px]'
portalToFollowElemContentClassName='z-[1000]' portalToFollowElemContentClassName='z-[1000]'
</div> </div>
<div> <div>
<div className='text-[0px]'> <div className='text-[0px]'>
<div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.codegen.instruction')}</div>
<Textarea
className="h-[200px] resize-none"
placeholder={t('appDebug.codegen.instructionPlaceholder') || ''}
<div className='system-sm-semibold-uppercase mb-1.5 text-text-secondary'>{t('appDebug.codegen.instruction')}</div>
<InstructionEditor
editorKey={editorKey}
value={instruction} value={instruction}
onChange={e => setInstruction(e.target.value)}
onChange={setInstruction}
nodeId={nodeId}
generatorType={GeneratorType.code}
isShowCurrentBlock={!!currentCode}
/> />
</div> </div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>


<div className='mt-5 flex justify-end'>
<div className='mt-7 flex justify-end space-x-2'>
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`)}</Button>
<Button <Button
className='flex space-x-1' className='flex space-x-1'
variant='primary' variant='primary'
onClick={onGenerate} onClick={onGenerate}
disabled={isLoading} disabled={isLoading}
> >
<Generator className='h-4 w-4 text-white' />
<span className='text-xs font-semibold text-white'>{t('appDebug.codegen.generate')}</span>
<Generator className='h-4 w-4' />
<span className='text-xs font-semibold '>{t('appDebug.codegen.generate')}</span>
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
{isLoading && renderLoading} {isLoading && renderLoading}
{!isLoading && !res && renderNoData}
{(!isLoading && res) && (
<div className='h-full w-0 grow p-6 pb-0'>
<div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.codegen.resTitle')}</div>
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
<ConfigPrompt
mode={mode}
promptTemplate={res?.code || ''}
promptVariables={[]}
readonly
noTitle={isInLLMNode}
gradientBorder
editorHeight={isInLLMNode ? 524 : 0}
noResize={isInLLMNode}
/>
{!isInLLMNode && (
<>
{res?.code && (
<div className='mt-4'>
<h3 className='mb-2 text-sm font-medium text-text-primary'>{t('appDebug.codegen.generatedCode')}</h3>
<pre className='overflow-x-auto rounded-lg bg-gray-50 p-4'>
<code className={`language-${res.language}`}>
{res.code}
</code>
</pre>
</div>
)}
{res?.error && (
<div className='mt-4 rounded-lg bg-red-50 p-4'>
<p className='text-sm text-red-600'>{res.error}</p>
</div>
)}
</>
)}
</div>

<div className='flex justify-end bg-background-default py-4'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='ml-2' onClick={() => {
setShowConfirmOverwrite(true)
}}>{t('appDebug.codegen.apply')}</Button>
</div>
{!isLoading && !current && <ResPlaceholder />}
{(!isLoading && current) && (
<div className='h-full w-0 grow bg-background-default-subtle p-6 pb-0'>
<Result
current={current!}
currentVersionIndex={currentVersionIndex || 0}
setCurrentVersionIndex={setCurrentVersionIndex}
versions={versions || []}
onApply={showConfirmOverwrite}
generatorType={GeneratorType.code}
/>
</div> </div>
)} )}
</div> </div>
{showConfirmOverwrite && (
{isShowConfirmOverwrite && (
<Confirm <Confirm
title={t('appDebug.codegen.overwriteConfirmTitle')} title={t('appDebug.codegen.overwriteConfirmTitle')}
content={t('appDebug.codegen.overwriteConfirmMessage')} content={t('appDebug.codegen.overwriteConfirmMessage')}
isShow={showConfirmOverwrite}
isShow
onConfirm={() => { onConfirm={() => {
setShowConfirmOverwrite(false)
onFinished(res!)
hideShowConfirmOverwrite()
onFinished(current!)
}} }}
onCancel={() => setShowConfirmOverwrite(false)}
onCancel={hideShowConfirmOverwrite}
/> />
)} )}
</Modal> </Modal>

+ 0
- 43
web/app/components/app/configuration/features/experience-enhance-group/index.tsx Zobrazit soubor

'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import GroupName from '../../base/group-name'
import TextToSpeech from '../chat-group/text-to-speech'
import MoreLikeThis from './more-like-this'

/*
* Include
* 1. More like this
*/

type ExperienceGroupProps = {
isShowTextToSpeech: boolean
isShowMoreLike: boolean
}

const ExperienceEnhanceGroup: FC<ExperienceGroupProps> = ({
isShowTextToSpeech,
isShowMoreLike,
}) => {
const { t } = useTranslation()

return (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.groupExperience.title')}/>
<div className='space-y-3'>
{
isShowMoreLike && (
<MoreLikeThis/>
)
}
{
isShowTextToSpeech && (
<TextToSpeech/>
)
}
</div>
</div>
)
}
export default React.memo(ExperienceEnhanceGroup)

+ 0
- 51
web/app/components/app/configuration/features/experience-enhance-group/more-like-this/index.tsx Zobrazit soubor

'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { XMarkIcon } from '@heroicons/react/24/outline'
import { useLocalStorageState } from 'ahooks'
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
import Panel from '@/app/components/app/configuration/base/feature-panel'

const GENERATE_NUM = 1

const warningIcon = (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M6.40616 0.834307C6.14751 0.719294 5.85222 0.719294 5.59356 0.834307C5.3938 0.923133 5.26403 1.07959 5.17373 1.20708C5.08495 1.33242 4.9899 1.49664 4.88536 1.67723L0.751783 8.81705C0.646828 8.9983 0.551451 9.16302 0.486781 9.3028C0.421056 9.44487 0.349754 9.63584 0.372478 9.85381C0.401884 10.1359 0.549654 10.3922 0.779012 10.5589C0.956259 10.6878 1.15726 10.7218 1.31314 10.7361C1.46651 10.7501 1.65684 10.7501 1.86628 10.7501H10.1334C10.3429 10.7501 10.5332 10.7501 10.6866 10.7361C10.8425 10.7218 11.0435 10.6878 11.2207 10.5589C11.4501 10.3922 11.5978 10.1359 11.6272 9.85381C11.65 9.63584 11.5787 9.44487 11.5129 9.3028C11.4483 9.16303 11.3529 8.99833 11.248 8.81709L7.11436 1.67722C7.00983 1.49663 6.91477 1.33242 6.82599 1.20708C6.73569 1.07959 6.60593 0.923133 6.40616 0.834307ZM6.49988 4.50012C6.49988 4.22398 6.27602 4.00012 5.99988 4.00012C5.72374 4.00012 5.49988 4.22398 5.49988 4.50012V6.50012C5.49988 6.77626 5.72374 7.00012 5.99988 7.00012C6.27602 7.00012 6.49988 6.77626 6.49988 6.50012V4.50012ZM5.99988 8.00012C5.72374 8.00012 5.49988 8.22398 5.49988 8.50012C5.49988 8.77626 5.72374 9.00012 5.99988 9.00012H6.00488C6.28102 9.00012 6.50488 8.77626 6.50488 8.50012C6.50488 8.22398 6.28102 8.00012 6.00488 8.00012H5.99988Z" fill="#F79009" />
</svg>

)
const MoreLikeThis: FC = () => {
const { t } = useTranslation()

const [isHideTip, setIsHideTip] = useLocalStorageState('isHideMoreLikeThisTip', {
defaultValue: false,
})

const headerRight = (
<div className='text-xs text-gray-500'>{t('appDebug.feature.moreLikeThis.generateNumTip')} {GENERATE_NUM}</div>
)
return (
<Panel
className='mt-4'
title={t('appDebug.feature.moreLikeThis.title')}
headerIcon={<MoreLikeThisIcon />}
headerRight={headerRight}
noBodySpacing
>
{!isHideTip && (
<div className='flex h-9 items-center justify-between rounded-b-xl bg-[#FFFAEB] px-3 text-xs text-gray-700'>
<div className='flex items-center space-x-2'>
<div>{warningIcon}</div>
<div>{t('appDebug.feature.moreLikeThis.tip')}</div>
</div>
<div className='flex h-4 w-4 cursor-pointer items-center justify-center' onClick={() => setIsHideTip(true)}>
<XMarkIcon className="h-3 w-3" />
</div>
</div>
)}

</Panel>
)
}
export default React.memo(MoreLikeThis)

+ 6
- 0
web/app/components/base/icons/assets/vender/line/general/code-assistant.svg Zobrazit soubor

<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z" fill="black"/>
<path d="M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z" fill="black"/>
<path d="M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z" fill="black"/>
<path d="M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z" fill="black"/>
</svg>

+ 6
- 0
web/app/components/base/icons/assets/vender/line/general/magic-edit.svg Zobrazit soubor

<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z" fill="black"/>
<path d="M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z" fill="black"/>
<path d="M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z" fill="black"/>
<path d="M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z" fill="black"/>
</svg>

+ 53
- 0
web/app/components/base/icons/src/vender/line/general/CodeAssistant.json Zobrazit soubor

{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M12 3C12.5523 3 13 3.44772 13 4C13 4.55228 12.5523 5 12 5H5.59962C5.30336 5 5.14096 5.00122 5.02443 5.01074C5.01998 5.01111 5.01573 5.01135 5.01173 5.01172C5.01136 5.01572 5.01112 5.01996 5.01076 5.02441C5.00124 5.14095 5.00001 5.30334 5.00001 5.59961V18.4004C5.00001 18.6967 5.00124 18.8591 5.01076 18.9756C5.01109 18.9797 5.01139 18.9836 5.01173 18.9873C5.01575 18.9877 5.01995 18.9889 5.02443 18.9893C5.14096 18.9988 5.30336 19 5.59962 19H18.4004C18.6967 19 18.8591 18.9988 18.9756 18.9893C18.9797 18.9889 18.9836 18.9876 18.9873 18.9873C18.9877 18.9836 18.9889 18.9797 18.9893 18.9756C18.9988 18.8591 19 18.6967 19 18.4004V12C19 11.4477 19.4477 11 20 11C20.5523 11 21 11.4477 21 12V18.4004C21 18.6638 21.0011 18.9219 20.9834 19.1387C20.9647 19.3672 20.9205 19.6369 20.7822 19.9082C20.5905 20.2845 20.2845 20.5905 19.9082 20.7822C19.6369 20.9205 19.3672 20.9647 19.1387 20.9834C18.9219 21.0011 18.6638 21 18.4004 21H5.59962C5.33625 21 5.07815 21.0011 4.86134 20.9834C4.63284 20.9647 4.36307 20.9204 4.09181 20.7822C3.71579 20.5906 3.40963 20.2847 3.21779 19.9082C3.07958 19.6369 3.03531 19.3672 3.01662 19.1387C2.9989 18.9219 3.00001 18.6638 3.00001 18.4004V5.59961C3.00001 5.33623 2.9989 5.07814 3.01662 4.86133C3.03531 4.63283 3.07958 4.36305 3.21779 4.0918C3.40953 3.71555 3.71557 3.40952 4.09181 3.21777C4.36306 3.07957 4.63285 3.0353 4.86134 3.0166C5.07815 2.99889 5.33624 3 5.59962 3H12Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13.293 9.29297C13.6835 8.90244 14.3165 8.90244 14.707 9.29297L16.707 11.293C17.0975 11.6835 17.0975 12.3165 16.707 12.707L14.707 14.707C14.3165 15.0975 13.6835 15.0975 13.293 14.707C12.9025 14.3165 12.9025 13.6835 13.293 13.293L14.586 12L13.293 10.707C12.9025 10.3165 12.9025 9.68349 13.293 9.29297Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.29298 9.29297C9.68351 8.90244 10.3165 8.90244 10.707 9.29297C11.0975 9.6835 11.0975 10.3165 10.707 10.707L9.41408 12L10.707 13.293L10.7754 13.3691C11.0957 13.7619 11.0731 14.3409 10.707 14.707C10.341 15.0731 9.76192 15.0957 9.36915 14.7754L9.29298 14.707L7.29298 12.707C6.90246 12.3165 6.90246 11.6835 7.29298 11.293L9.29298 9.29297Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M18.501 2C18.7487 2.00048 18.9564 2.18654 18.9844 2.43262C19.1561 3.94883 20.0165 4.87694 21.5557 5.01367C21.8075 5.03614 22.0003 5.24816 22 5.50098C21.9995 5.75356 21.8063 5.96437 21.5547 5.98633C20.0372 6.11768 19.1176 7.03726 18.9863 8.55469C18.9644 8.80629 18.7536 8.99948 18.501 9C18.2483 9.00028 18.0362 8.80743 18.0137 8.55566C17.877 7.01647 16.9488 6.15613 15.4326 5.98438C15.1866 5.95634 15.0006 5.74863 15 5.50098C14.9997 5.25311 15.1855 5.04417 15.4317 5.01563C16.9697 4.83823 17.8382 3.96966 18.0156 2.43164C18.0442 2.18545 18.2531 1.99975 18.501 2Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "CodeAssistant"
}

+ 20
- 0
web/app/components/base/icons/src/vender/line/general/CodeAssistant.tsx Zobrazit soubor

// GENERATE BY script
// DON NOT EDIT IT MANUALLY

import * as React from 'react'
import data from './CodeAssistant.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'

const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'CodeAssistant'

export default Icon

+ 55
- 0
web/app/components/base/icons/src/vender/line/general/MagicEdit.json Zobrazit soubor

{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M15.3691 3.22448C15.7619 2.90422 16.3409 2.92682 16.707 3.29284L20.707 7.29284C21.0975 7.68334 21.0975 8.31637 20.707 8.7069L8.70703 20.7069C8.5195 20.8944 8.26521 20.9999 8 20.9999H4C3.44771 20.9999 3 20.5522 3 19.9999V15.9999L3.00488 15.9012C3.0276 15.6723 3.12886 15.4569 3.29297 15.2928L15.293 3.29284L15.3691 3.22448ZM5 16.4139V18.9999H7.58593L18.5859 7.99987L16 5.41394L5 16.4139Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M18.2451 15.1581C18.3502 14.9478 18.6498 14.9478 18.7549 15.1581L19.4082 16.4637C19.4358 16.5189 19.4809 16.5641 19.5361 16.5917L20.8428 17.245C21.0525 17.3502 21.0524 17.6495 20.8428 17.7548L19.5361 18.4081C19.4809 18.4357 19.4358 18.4808 19.4082 18.536L18.7549 19.8426C18.6497 20.0525 18.3503 20.0525 18.2451 19.8426L17.5918 18.536C17.5642 18.4808 17.5191 18.4357 17.4639 18.4081L16.1572 17.7548C15.9476 17.6495 15.9475 17.3502 16.1572 17.245L17.4639 16.5917C17.5191 16.5641 17.5642 16.5189 17.5918 16.4637L18.2451 15.1581Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.24511 5.15808C4.35024 4.94783 4.64975 4.94783 4.75488 5.15808L5.4082 6.46374C5.4358 6.51895 5.48092 6.56406 5.53613 6.59167L6.84277 7.24499C7.05248 7.3502 7.05238 7.64946 6.84277 7.75476L5.53613 8.40808C5.48092 8.43568 5.4358 8.4808 5.4082 8.53601L4.75488 9.84265C4.64966 10.0525 4.35033 10.0525 4.24511 9.84265L3.59179 8.53601C3.56418 8.4808 3.51907 8.43568 3.46386 8.40808L2.15722 7.75476C1.94764 7.64945 1.94755 7.35021 2.15722 7.24499L3.46386 6.59167C3.51907 6.56406 3.56418 6.51895 3.59179 6.46374L4.24511 5.15808Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M8.73535 2.16394C8.84432 1.94601 9.15568 1.94601 9.26465 2.16394L9.74414 3.12292C9.77275 3.18014 9.81973 3.22712 9.87695 3.25573L10.8369 3.73522C11.0546 3.84424 11.0545 4.15544 10.8369 4.26452L9.87695 4.74401C9.81973 4.77262 9.77275 4.81961 9.74414 4.87683L9.26465 5.83679C9.15565 6.05458 8.84435 6.05457 8.73535 5.83679L8.25586 4.87683C8.22725 4.81961 8.18026 4.77262 8.12304 4.74401L7.16308 4.26452C6.94547 4.15546 6.94539 3.84422 7.16308 3.73522L8.12304 3.25573C8.18026 3.22712 8.22725 3.18014 8.25586 3.12292L8.73535 2.16394Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "MagicEdit"
}

+ 20
- 0
web/app/components/base/icons/src/vender/line/general/MagicEdit.tsx Zobrazit soubor

// GENERATE BY script
// DON NOT EDIT IT MANUALLY

import * as React from 'react'
import data from './MagicEdit.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'

const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

Icon.displayName = 'MagicEdit'

export default Icon

+ 2
- 0
web/app/components/base/icons/src/vender/line/general/index.ts Zobrazit soubor

export { default as CheckDone01 } from './CheckDone01' export { default as CheckDone01 } from './CheckDone01'
export { default as Check } from './Check' export { default as Check } from './Check'
export { default as ChecklistSquare } from './ChecklistSquare' export { default as ChecklistSquare } from './ChecklistSquare'
export { default as CodeAssistant } from './CodeAssistant'
export { default as DotsGrid } from './DotsGrid' export { default as DotsGrid } from './DotsGrid'
export { default as Edit02 } from './Edit02' export { default as Edit02 } from './Edit02'
export { default as Edit04 } from './Edit04' export { default as Edit04 } from './Edit04'
export { default as LogIn04 } from './LogIn04' export { default as LogIn04 } from './LogIn04'
export { default as LogOut01 } from './LogOut01' export { default as LogOut01 } from './LogOut01'
export { default as LogOut04 } from './LogOut04' export { default as LogOut04 } from './LogOut04'
export { default as MagicEdit } from './MagicEdit'
export { default as Menu01 } from './Menu01' export { default as Menu01 } from './Menu01'
export { default as Pin01 } from './Pin01' export { default as Pin01 } from './Pin01'
export { default as Pin02 } from './Pin02' export { default as Pin02 } from './Pin02'

+ 4
- 0
web/app/components/base/prompt-editor/constants.tsx Zobrazit soubor

export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}' export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}'
export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}' export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}'
export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}' export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}'
export const CURRENT_PLACEHOLDER_TEXT = '{{#current#}}'
export const ERROR_MESSAGE_PLACEHOLDER_TEXT = '{{#error_message#}}'
export const LAST_RUN_PLACEHOLDER_TEXT = '{{#last_run#}}'

export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}' export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets' export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role' export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role'

+ 66
- 3
web/app/components/base/prompt-editor/index.tsx Zobrazit soubor

'use client' 'use client'


import type { FC, ReactNode } from 'react'
import { useEffect } from 'react'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import type { import type {
EditorState, EditorState,
} from 'lexical' } from 'lexical'
WorkflowVariableBlockNode, WorkflowVariableBlockNode,
WorkflowVariableBlockReplacementBlock, WorkflowVariableBlockReplacementBlock,
} from './plugins/workflow-variable-block' } from './plugins/workflow-variable-block'
import {
CurrentBlock,
CurrentBlockNode,
CurrentBlockReplacementBlock,
} from './plugins/current-block'
import {
ErrorMessageBlock,
ErrorMessageBlockNode,
ErrorMessageBlockReplacementBlock,
} from './plugins/error-message-block'
import {
LastRunBlock,
LastRunBlockNode,
LastRunReplacementBlock,
} from './plugins/last-run-block'

import VariableBlock from './plugins/variable-block' import VariableBlock from './plugins/variable-block'
import VariableValueBlock from './plugins/variable-value-block' import VariableValueBlock from './plugins/variable-value-block'
import { VariableValueBlockNode } from './plugins/variable-value-block/node' import { VariableValueBlockNode } from './plugins/variable-value-block/node'
import { textToEditorState } from './utils' import { textToEditorState } from './utils'
import type { import type {
ContextBlockType, ContextBlockType,
CurrentBlockType,
ErrorMessageBlockType,
ExternalToolBlockType, ExternalToolBlockType,
HistoryBlockType, HistoryBlockType,
LastRunBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType,
compact?: boolean compact?: boolean
wrapperClassName?: string wrapperClassName?: string
className?: string className?: string
placeholder?: string | ReactNode
placeholder?: string | React.ReactNode
placeholderClassName?: string placeholderClassName?: string
style?: React.CSSProperties style?: React.CSSProperties
value?: string value?: string
variableBlock?: VariableBlockType variableBlock?: VariableBlockType
externalToolBlock?: ExternalToolBlockType externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType workflowVariableBlock?: WorkflowVariableBlockType
currentBlock?: CurrentBlockType
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean isSupportFileVar?: boolean
} }


variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock,
currentBlock,
errorMessageBlock,
lastRunBlock,
isSupportFileVar, isSupportFileVar,
}) => { }) => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
QueryBlockNode, QueryBlockNode,
WorkflowVariableBlockNode, WorkflowVariableBlockNode,
VariableValueBlockNode, VariableValueBlockNode,
CurrentBlockNode,
ErrorMessageBlockNode,
LastRunBlockNode, // LastRunBlockNode is used for error message block replacement
], ],
editorState: textToEditorState(value || ''), editorState: textToEditorState(value || ''),
onError: (error: Error) => { onError: (error: Error) => {
variableBlock={variableBlock} variableBlock={variableBlock}
externalToolBlock={externalToolBlock} externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock} workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar} isSupportFileVar={isSupportFileVar}
/> />
<ComponentPickerBlock <ComponentPickerBlock
variableBlock={variableBlock} variableBlock={variableBlock}
externalToolBlock={externalToolBlock} externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock} workflowVariableBlock={workflowVariableBlock}
currentBlock={currentBlock}
errorMessageBlock={errorMessageBlock}
lastRunBlock={lastRunBlock}
isSupportFileVar={isSupportFileVar} isSupportFileVar={isSupportFileVar}
/> />
{ {
</> </>
) )
} }
{
currentBlock?.show && (
<>
<CurrentBlock {...currentBlock} />
<CurrentBlockReplacementBlock {...currentBlock} />
</>
)
}
{
errorMessageBlock?.show && (
<>
<ErrorMessageBlock {...errorMessageBlock} />
<ErrorMessageBlockReplacementBlock {...errorMessageBlock} />
</>
)
}
{
lastRunBlock?.show && (
<>
<LastRunBlock {...lastRunBlock} />
<LastRunReplacementBlock {...lastRunBlock} />
</>
)
}
{
isSupportFileVar && (
<VariableValueBlock />
)
}
<OnChangePlugin onChange={handleEditorChange} /> <OnChangePlugin onChange={handleEditorChange} />
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} /> <OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
<UpdateBlock instanceId={instanceId} /> <UpdateBlock instanceId={instanceId} />

+ 51
- 3
web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx Zobrazit soubor

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { import type {
ContextBlockType, ContextBlockType,
CurrentBlockType,
ErrorMessageBlockType,
ExternalToolBlockType, ExternalToolBlockType,
HistoryBlockType, HistoryBlockType,
LastRunBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType,
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users' import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import { VarType } from '@/app/components/workflow/types'


export const usePromptOptions = ( export const usePromptOptions = (
contextBlock?: ContextBlockType, contextBlock?: ContextBlockType,
variableBlock?: VariableBlockType, variableBlock?: VariableBlockType,
externalToolBlockType?: ExternalToolBlockType, externalToolBlockType?: ExternalToolBlockType,
workflowVariableBlockType?: WorkflowVariableBlockType, workflowVariableBlockType?: WorkflowVariableBlockType,
currentBlockType?: CurrentBlockType,
errorMessageBlockType?: ErrorMessageBlockType,
lastRunBlockType?: LastRunBlockType,
queryString?: string, queryString?: string,
) => { ) => {
const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock) const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
const variableOptions = useVariableOptions(variableBlock, queryString) const variableOptions = useVariableOptions(variableBlock, queryString)
const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString) const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)

const workflowVariableOptions = useMemo(() => { const workflowVariableOptions = useMemo(() => {
if (!workflowVariableBlockType?.show) if (!workflowVariableBlockType?.show)
return [] return []

return workflowVariableBlockType.variables || []
}, [workflowVariableBlockType])
const res = workflowVariableBlockType.variables || []
if(errorMessageBlockType?.show && res.findIndex(v => v.nodeId === 'error_message') === -1) {
res.unshift({
nodeId: 'error_message',
title: 'error_message',
isFlat: true,
vars: [
{
variable: 'error_message',
type: VarType.string,
},
],
})
}
if(lastRunBlockType?.show && res.findIndex(v => v.nodeId === 'last_run') === -1) {
res.unshift({
nodeId: 'last_run',
title: 'last_run',
isFlat: true,
vars: [
{
variable: 'last_run',
type: VarType.object,
},
],
})
}
if(currentBlockType?.show && res.findIndex(v => v.nodeId === 'current') === -1) {
const title = currentBlockType.generatorType === 'prompt' ? 'current_prompt' : 'current_code'
res.unshift({
nodeId: 'current',
title,
isFlat: true,
vars: [
{
variable: 'current',
type: VarType.string,
},
],
})
}
return res
}, [workflowVariableBlockType?.show, workflowVariableBlockType?.variables, errorMessageBlockType?.show, lastRunBlockType?.show, currentBlockType?.show, currentBlockType?.generatorType])


return useMemo(() => { return useMemo(() => {
return { return {

+ 33
- 5
web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx Zobrazit soubor

import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin' import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import type { import type {
ContextBlockType, ContextBlockType,
CurrentBlockType,
ErrorMessageBlockType,
ExternalToolBlockType, ExternalToolBlockType,
HistoryBlockType, HistoryBlockType,
LastRunBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType,
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import { KEY_ESCAPE_COMMAND } from 'lexical' import { KEY_ESCAPE_COMMAND } from 'lexical'
import { INSERT_CURRENT_BLOCK_COMMAND } from '../current-block'
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
import { INSERT_ERROR_MESSAGE_BLOCK_COMMAND } from '../error-message-block'
import { INSERT_LAST_RUN_BLOCK_COMMAND } from '../last-run-block'


type ComponentPickerProps = { type ComponentPickerProps = {
triggerString: string triggerString: string
variableBlock?: VariableBlockType variableBlock?: VariableBlockType
externalToolBlock?: ExternalToolBlockType externalToolBlock?: ExternalToolBlockType
workflowVariableBlock?: WorkflowVariableBlockType workflowVariableBlock?: WorkflowVariableBlockType
currentBlock?: CurrentBlockType
errorMessageBlock?: ErrorMessageBlockType
lastRunBlock?: LastRunBlockType
isSupportFileVar?: boolean isSupportFileVar?: boolean
} }
const ComponentPicker = ({ const ComponentPicker = ({
variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock,
currentBlock,
errorMessageBlock,
lastRunBlock,
isSupportFileVar, isSupportFileVar,
}: ComponentPickerProps) => { }: ComponentPickerProps) => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock,
currentBlock,
errorMessageBlock,
lastRunBlock,
) )


const onSelectOption = useCallback( const onSelectOption = useCallback(
if (needRemove) if (needRemove)
needRemove.remove() needRemove.remove()
}) })

if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
const isFlat = variables.length === 1
if (isFlat) {
const varName = variables[0]
if (varName === 'current')
editor.dispatchCommand(INSERT_CURRENT_BLOCK_COMMAND, currentBlock?.generatorType)
else if (varName === 'error_message')
editor.dispatchCommand(INSERT_ERROR_MESSAGE_BLOCK_COMMAND, null)
else if (varName === 'last_run')
editor.dispatchCommand(INSERT_LAST_RUN_BLOCK_COMMAND, null)
}
else if (variables[1] === 'sys.query' || variables[1] === 'sys.files') {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]]) editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
else
}
else {
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables) editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
}, [editor, checkForTriggerMatch, triggerString])
}
}, [editor, currentBlock?.generatorType, checkForTriggerMatch, triggerString])


const handleClose = useCallback(() => { const handleClose = useCallback(() => {
const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' }) const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' })
showManageInputField={workflowVariableBlock.showManageInputField} showManageInputField={workflowVariableBlock.showManageInputField}
onManageInputField={workflowVariableBlock.onManageInputField} onManageInputField={workflowVariableBlock.onManageInputField}
autoFocus={false} autoFocus={false}
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
/> />
</div> </div>
) )
} }
</> </>
) )
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable, handleClose, isSupportFileVar, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])
}, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField])


return ( return (
<LexicalTypeaheadMenuPlugin <LexicalTypeaheadMenuPlugin

+ 44
- 0
web/app/components/base/prompt-editor/plugins/current-block/component.tsx Zobrazit soubor

import { type FC, useEffect } from 'react'
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useSelectOrDelete } from '../../hooks'
import { CurrentBlockNode, DELETE_CURRENT_BLOCK_COMMAND } from '.'
import cn from '@/utils/classnames'
import { CodeAssistant, MagicEdit } from '../../../icons/src/vender/line/general'

type CurrentBlockComponentProps = {
nodeKey: string
generatorType: GeneratorType
}

const CurrentBlockComponent: FC<CurrentBlockComponentProps> = ({
nodeKey,
generatorType,
}) => {
const [editor] = useLexicalComposerContext()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CURRENT_BLOCK_COMMAND)

const Icon = generatorType === GeneratorType.prompt ? MagicEdit : CodeAssistant
useEffect(() => {
if (!editor.hasNodes([CurrentBlockNode]))
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
}, [editor])

return (
<div
className={cn(
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-violet-violet-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
)}
onClick={(e) => {
e.stopPropagation()
}}
ref={ref}
>
<Icon className='mr-0.5 h-[14px] w-[14px]' />
<div className='text-xs font-medium'>{generatorType === GeneratorType.prompt ? 'current_prompt' : 'current_code'}</div>
</div>
)
}

export default CurrentBlockComponent

+ 61
- 0
web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx Zobrazit soubor

import {
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { CURRENT_PLACEHOLDER_TEXT } from '../../constants'
import type { CurrentBlockType } from '../../types'
import {
$createCurrentBlockNode,
CurrentBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node'

const REGEX = new RegExp(CURRENT_PLACEHOLDER_TEXT)

const CurrentBlockReplacementBlock = ({
generatorType,
onInsert,
}: CurrentBlockType) => {
const [editor] = useLexicalComposerContext()

useEffect(() => {
if (!editor.hasNodes([CurrentBlockNode]))
throw new Error('CurrentBlockNodePlugin: CurrentBlockNode not registered on editor')
}, [editor])

const createCurrentBlockNode = useCallback((): CurrentBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createCurrentBlockNode(generatorType))
}, [onInsert, generatorType])

const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)

if (matchArr === null)
return null

const startOffset = matchArr.index
const endOffset = startOffset + CURRENT_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])

useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)),
)
}, [])

return null
}

export default memo(CurrentBlockReplacementBlock)

+ 66
- 0
web/app/components/base/prompt-editor/plugins/current-block/index.tsx Zobrazit soubor

import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { CurrentBlockType } from '../../types'
import {
$createCurrentBlockNode,
CurrentBlockNode,
} from './node'

export const INSERT_CURRENT_BLOCK_COMMAND = createCommand('INSERT_CURRENT_BLOCK_COMMAND')
export const DELETE_CURRENT_BLOCK_COMMAND = createCommand('DELETE_CURRENT_BLOCK_COMMAND')

const CurrentBlock = memo(({
generatorType,
onInsert,
onDelete,
}: CurrentBlockType) => {
const [editor] = useLexicalComposerContext()

useEffect(() => {
if (!editor.hasNodes([CurrentBlockNode]))
throw new Error('CURRENTBlockPlugin: CURRENTBlock not registered on editor')

return mergeRegister(
editor.registerCommand(
INSERT_CURRENT_BLOCK_COMMAND,
() => {
const currentBlockNode = $createCurrentBlockNode(generatorType)

$insertNodes([currentBlockNode])

if (onInsert)
onInsert()

return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_CURRENT_BLOCK_COMMAND,
() => {
if (onDelete)
onDelete()

return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, generatorType, onDelete, onInsert])

return null
})
CurrentBlock.displayName = 'CurrentBlock'

export { CurrentBlock }
export { CurrentBlockNode } from './node'
export { default as CurrentBlockReplacementBlock } from './current-block-replacement-block'

+ 78
- 0
web/app/components/base/prompt-editor/plugins/current-block/node.tsx Zobrazit soubor

import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import CurrentBlockComponent from './component'
import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'

export type SerializedNode = SerializedLexicalNode & { generatorType: GeneratorType; }

export class CurrentBlockNode extends DecoratorNode<React.JSX.Element> {
__generatorType: GeneratorType
static getType(): string {
return 'current-block'
}

static clone(node: CurrentBlockNode): CurrentBlockNode {
return new CurrentBlockNode(node.__generatorType, node.getKey())
}

isInline(): boolean {
return true
}

constructor(generatorType: GeneratorType, key?: NodeKey) {
super(key)

this.__generatorType = generatorType
}

createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}

updateDOM(): false {
return false
}

decorate(): React.JSX.Element {
return (
<CurrentBlockComponent
nodeKey={this.getKey()}
generatorType={this.getGeneratorType()}
/>
)
}

getGeneratorType(): GeneratorType {
const self = this.getLatest()
return self.__generatorType
}

static importJSON(serializedNode: SerializedNode): CurrentBlockNode {
const node = $createCurrentBlockNode(serializedNode.generatorType)

return node
}

exportJSON(): SerializedNode {
return {
type: 'current-block',
version: 1,
generatorType: this.getGeneratorType(),
}
}

getTextContent(): string {
return '{{#current#}}'
}
}
export function $createCurrentBlockNode(type: GeneratorType): CurrentBlockNode {
return new CurrentBlockNode(type)
}

export function $isCurrentBlockNode(
node: CurrentBlockNode | LexicalNode | null | undefined,
): boolean {
return node instanceof CurrentBlockNode
}

+ 40
- 0
web/app/components/base/prompt-editor/plugins/error-message-block/component.tsx Zobrazit soubor

import { type FC, useEffect } from 'react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useSelectOrDelete } from '../../hooks'
import { DELETE_ERROR_MESSAGE_COMMAND, ErrorMessageBlockNode } from '.'
import cn from '@/utils/classnames'
import { Variable02 } from '../../../icons/src/vender/solid/development'

type Props = {
nodeKey: string
}

const ErrorMessageBlockComponent: FC<Props> = ({
nodeKey,
}) => {
const [editor] = useLexicalComposerContext()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_ERROR_MESSAGE_COMMAND)

useEffect(() => {
if (!editor.hasNodes([ErrorMessageBlockNode]))
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
}, [editor])

return (
<div
className={cn(
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-util-colors-orange-dark-orange-dark-600 hover:border-state-accent-solid hover:bg-state-accent-hover',
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
)}
onClick={(e) => {
e.stopPropagation()
}}
ref={ref}
>
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
<div className='text-xs font-medium'>error_message</div>
</div>
)
}

export default ErrorMessageBlockComponent

+ 60
- 0
web/app/components/base/prompt-editor/plugins/error-message-block/error-message-block-replacement-block.tsx Zobrazit soubor

import {
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { ERROR_MESSAGE_PLACEHOLDER_TEXT } from '../../constants'
import type { ErrorMessageBlockType } from '../../types'
import {
$createErrorMessageBlockNode,
ErrorMessageBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node'

const REGEX = new RegExp(ERROR_MESSAGE_PLACEHOLDER_TEXT)

const ErrorMessageBlockReplacementBlock = ({
onInsert,
}: ErrorMessageBlockType) => {
const [editor] = useLexicalComposerContext()

useEffect(() => {
if (!editor.hasNodes([ErrorMessageBlockNode]))
throw new Error('ErrorMessageBlockNodePlugin: ErrorMessageBlockNode not registered on editor')
}, [editor])

const createErrorMessageBlockNode = useCallback((): ErrorMessageBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createErrorMessageBlockNode())
}, [onInsert])

const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)

if (matchArr === null)
return null

const startOffset = matchArr.index
const endOffset = startOffset + ERROR_MESSAGE_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])

useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createErrorMessageBlockNode)),
)
}, [])

return null
}

export default memo(ErrorMessageBlockReplacementBlock)

+ 65
- 0
web/app/components/base/prompt-editor/plugins/error-message-block/index.tsx Zobrazit soubor

import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { ErrorMessageBlockType } from '../../types'
import {
$createErrorMessageBlockNode,
ErrorMessageBlockNode,
} from './node'

export const INSERT_ERROR_MESSAGE_BLOCK_COMMAND = createCommand('INSERT_ERROR_MESSAGE_BLOCK_COMMAND')
export const DELETE_ERROR_MESSAGE_COMMAND = createCommand('DELETE_ERROR_MESSAGE_COMMAND')

const ErrorMessageBlock = memo(({
onInsert,
onDelete,
}: ErrorMessageBlockType) => {
const [editor] = useLexicalComposerContext()

useEffect(() => {
if (!editor.hasNodes([ErrorMessageBlockNode]))
throw new Error('ERROR_MESSAGEBlockPlugin: ERROR_MESSAGEBlock not registered on editor')

return mergeRegister(
editor.registerCommand(
INSERT_ERROR_MESSAGE_BLOCK_COMMAND,
() => {
const Node = $createErrorMessageBlockNode()

$insertNodes([Node])

if (onInsert)
onInsert()

return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_ERROR_MESSAGE_COMMAND,
() => {
if (onDelete)
onDelete()

return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onDelete, onInsert])

return null
})
ErrorMessageBlock.displayName = 'ErrorMessageBlock'

export { ErrorMessageBlock }
export { ErrorMessageBlockNode } from './node'
export { default as ErrorMessageBlockReplacementBlock } from './error-message-block-replacement-block'

+ 67
- 0
web/app/components/base/prompt-editor/plugins/error-message-block/node.tsx Zobrazit soubor

import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import ErrorMessageBlockComponent from './component'

export type SerializedNode = SerializedLexicalNode

export class ErrorMessageBlockNode extends DecoratorNode<React.JSX.Element> {
static getType(): string {
return 'error-message-block'
}

static clone(node: ErrorMessageBlockNode): ErrorMessageBlockNode {
return new ErrorMessageBlockNode(node.getKey())
}

isInline(): boolean {
return true
}

constructor(key?: NodeKey) {
super(key)
}

createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}

updateDOM(): false {
return false
}

decorate(): React.JSX.Element {
return (
<ErrorMessageBlockComponent
nodeKey={this.getKey()}
/>
)
}

static importJSON(): ErrorMessageBlockNode {
const node = $createErrorMessageBlockNode()

return node
}

exportJSON(): SerializedNode {
return {
type: 'error-message-block',
version: 1,
}
}

getTextContent(): string {
return '{{#error_message#}}'
}
}
export function $createErrorMessageBlockNode(): ErrorMessageBlockNode {
return new ErrorMessageBlockNode()
}

export function $isErrorMessageBlockNode(
node: ErrorMessageBlockNode | LexicalNode | null | undefined,
): boolean {
return node instanceof ErrorMessageBlockNode
}

+ 40
- 0
web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx Zobrazit soubor

import { type FC, useEffect } from 'react'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useSelectOrDelete } from '../../hooks'
import { DELETE_LAST_RUN_COMMAND, LastRunBlockNode } from '.'
import cn from '@/utils/classnames'
import { Variable02 } from '../../../icons/src/vender/solid/development'

type Props = {
nodeKey: string
}

const LastRunBlockComponent: FC<Props> = ({
nodeKey,
}) => {
const [editor] = useLexicalComposerContext()
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_LAST_RUN_COMMAND)

useEffect(() => {
if (!editor.hasNodes([LastRunBlockNode]))
throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor')
}, [editor])

return (
<div
className={cn(
'group/wrap relative mx-0.5 flex h-[18px] select-none items-center rounded-[5px] border pl-0.5 pr-[3px] text-text-accent hover:border-state-accent-solid hover:bg-state-accent-hover',
isSelected ? ' border-state-accent-solid bg-state-accent-hover' : ' border-components-panel-border-subtle bg-components-badge-white-to-dark',
)}
onClick={(e) => {
e.stopPropagation()
}}
ref={ref}
>
<Variable02 className='mr-0.5 h-[14px] w-[14px]' />
<div className='text-xs font-medium'>last_run</div>
</div>
)
}

export default LastRunBlockComponent

+ 65
- 0
web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx Zobrazit soubor

import {
memo,
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { LastRunBlockType } from '../../types'
import {
$createLastRunBlockNode,
LastRunBlockNode,
} from './node'

export const INSERT_LAST_RUN_BLOCK_COMMAND = createCommand('INSERT_LAST_RUN_BLOCK_COMMAND')
export const DELETE_LAST_RUN_COMMAND = createCommand('DELETE_LAST_RUN_COMMAND')

const LastRunBlock = memo(({
onInsert,
onDelete,
}: LastRunBlockType) => {
const [editor] = useLexicalComposerContext()

useEffect(() => {
if (!editor.hasNodes([LastRunBlockNode]))
throw new Error('Last_RunBlockPlugin: Last_RunBlock not registered on editor')

return mergeRegister(
editor.registerCommand(
INSERT_LAST_RUN_BLOCK_COMMAND,
() => {
const Node = $createLastRunBlockNode()

$insertNodes([Node])

if (onInsert)
onInsert()

return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
DELETE_LAST_RUN_COMMAND,
() => {
if (onDelete)
onDelete()

return true
},
COMMAND_PRIORITY_EDITOR,
),
)
}, [editor, onDelete, onInsert])

return null
})
LastRunBlock.displayName = 'LastRunBlock'

export { LastRunBlock }
export { LastRunBlockNode } from './node'
export { default as LastRunReplacementBlock } from './last-run-block-replacement-block'

+ 60
- 0
web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx Zobrazit soubor

import {
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils'
import { LAST_RUN_PLACEHOLDER_TEXT } from '../../constants'
import type { LastRunBlockType } from '../../types'
import {
$createLastRunBlockNode,
LastRunBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node'

const REGEX = new RegExp(LAST_RUN_PLACEHOLDER_TEXT)

const LastRunReplacementBlock = ({
onInsert,
}: LastRunBlockType) => {
const [editor] = useLexicalComposerContext()

useEffect(() => {
if (!editor.hasNodes([LastRunBlockNode]))
throw new Error('LastRunMessageBlockNodePlugin: LastRunMessageBlockNode not registered on editor')
}, [editor])

const createLastRunBlockNode = useCallback((): LastRunBlockNode => {
if (onInsert)
onInsert()
return $applyNodeReplacement($createLastRunBlockNode())
}, [onInsert])

const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text)

if (matchArr === null)
return null

const startOffset = matchArr.index
const endOffset = startOffset + LAST_RUN_PLACEHOLDER_TEXT.length
return {
end: endOffset,
start: startOffset,
}
}, [])

useEffect(() => {
REGEX.lastIndex = 0
return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)),
)
}, [])

return null
}

export default memo(LastRunReplacementBlock)

+ 67
- 0
web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx Zobrazit soubor

import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
import { DecoratorNode } from 'lexical'
import LastRunBlockComponent from './component'

export type SerializedNode = SerializedLexicalNode

export class LastRunBlockNode extends DecoratorNode<React.JSX.Element> {
static getType(): string {
return 'last-run-block'
}

static clone(node: LastRunBlockNode): LastRunBlockNode {
return new LastRunBlockNode(node.getKey())
}

isInline(): boolean {
return true
}

constructor(key?: NodeKey) {
super(key)
}

createDOM(): HTMLElement {
const div = document.createElement('div')
div.classList.add('inline-flex', 'items-center', 'align-middle')
return div
}

updateDOM(): false {
return false
}

decorate(): React.JSX.Element {
return (
<LastRunBlockComponent
nodeKey={this.getKey()}
/>
)
}

static importJSON(): LastRunBlockNode {
const node = $createLastRunBlockNode()

return node
}

exportJSON(): SerializedNode {
return {
type: 'last-run-block',
version: 1,
}
}

getTextContent(): string {
return '{{#last_run#}}'
}
}
export function $createLastRunBlockNode(): LastRunBlockNode {
return new LastRunBlockNode()
}

export function $isLastRunBlockNode(
node: LastRunBlockNode | LexicalNode | null | undefined,
): boolean {
return node instanceof LastRunBlockNode
}

+ 20
- 0
web/app/components/base/prompt-editor/types.ts Zobrazit soubor

import type { GeneratorType } from '../../app/configuration/config/automatic/types'
import type { Type } from '../../workflow/nodes/llm/types' import type { Type } from '../../workflow/nodes/llm/types'
import type { Dataset } from './plugins/context-block' import type { Dataset } from './plugins/context-block'
import type { RoleName } from './plugins/history-block' import type { RoleName } from './plugins/history-block'
matchingString: string matchingString: string
replaceableString: string replaceableString: string
} }

export type CurrentBlockType = {
show?: boolean
generatorType: GeneratorType
onInsert?: () => void
onDelete?: () => void
}

export type ErrorMessageBlockType = {
show?: boolean
onInsert?: () => void
onDelete?: () => void
}

export type LastRunBlockType = {
show?: boolean
onInsert?: () => void
onDelete?: () => void
}

+ 1
- 1
web/app/components/datasets/documents/detail/completed/common/tag.tsx Zobrazit soubor

return ( return (
<div className={cn('inline-flex items-center gap-x-0.5', className)}> <div className={cn('inline-flex items-center gap-x-0.5', className)}>
<span className='text-xs font-medium text-text-quaternary'>#</span> <span className='text-xs font-medium text-text-quaternary'>#</span>
<span className='line-clamp-1 max-w-12 shrink-0 text-xs text-text-tertiary'>{text}</span>
<span className='max-w-12 shrink-0 truncate text-xs text-text-tertiary'>{text}</span>
</div> </div>
) )
} }

+ 379
- 4
web/app/components/datasets/documents/list.tsx Zobrazit soubor

import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type' import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type'
import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata' import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata'
import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal' import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
import StatusItem from './status-item'
import Operations from './operations'
import { noop } from 'lodash-es'

export const useIndexStatus = () => {
const { t } = useTranslation()
return {
queuing: { color: 'orange', text: t('datasetDocuments.list.status.queuing') }, // waiting
indexing: { color: 'blue', text: t('datasetDocuments.list.status.indexing') }, // indexing splitting parsing cleaning
paused: { color: 'orange', text: t('datasetDocuments.list.status.paused') }, // paused
error: { color: 'red', text: t('datasetDocuments.list.status.error') }, // error
available: { color: 'green', text: t('datasetDocuments.list.status.available') }, // completed,archived = false,enabled = true
enabled: { color: 'green', text: t('datasetDocuments.list.status.enabled') }, // completed,archived = false,enabled = true
disabled: { color: 'gray', text: t('datasetDocuments.list.status.disabled') }, // completed,archived = false,enabled = false
archived: { color: 'gray', text: t('datasetDocuments.list.status.archived') }, // completed,archived = true
}
}

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',
}

// status item for list
export const StatusItem: FC<{
status: DocumentDisplayStatus
reverse?: boolean
scene?: 'list' | 'detail'
textCls?: string
errorMessage?: string
detail?: {
enabled: boolean
archived: boolean
id: string
}
datasetId?: string
onUpdate?: (operationName?: string) => void

}> = ({ status, reverse = false, scene = 'list', textCls = '', errorMessage, datasetId = '', detail, onUpdate }) => {
const DOC_INDEX_STATUS_MAP = useIndexStatus()
const localStatus = status.toLowerCase() as keyof typeof DOC_INDEX_STATUS_MAP
const { enabled = false, archived = false, id = '' } = detail || {}
const { notify } = useContext(ToastContext)
const { t } = useTranslation()
const { mutateAsync: enableDocument } = useDocumentEnable()
const { mutateAsync: disableDocument } = useDocumentDisable()
const { mutateAsync: deleteDocument } = useDocumentDelete()

const onOperate = async (operationName: OperationName) => {
let opApi = deleteDocument
switch (operationName) {
case 'enable':
opApi = enableDocument
break
case 'disable':
opApi = disableDocument
break
}
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: id }) as Promise<CommonResponse>)
if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onUpdate?.()
// onUpdate?.(operationName)
}
else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
}

const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => {
if (operationName === 'enable' && enabled)
return
if (operationName === 'disable' && !enabled)
return
onOperate(operationName)
}, { wait: 500 })

const embedding = useMemo(() => {
return ['queuing', 'indexing', 'paused'].includes(localStatus)
}, [localStatus])

return <div className={
cn('flex items-center',
reverse ? 'flex-row-reverse' : '',
scene === 'detail' ? s.statusItemDetail : '')
}>
<Indicator color={DOC_INDEX_STATUS_MAP[localStatus]?.color as IndicatorProps['color']} className={reverse ? 'ml-2' : 'mr-2'} />
<span className={cn(`${STATUS_TEXT_COLOR_MAP[DOC_INDEX_STATUS_MAP[localStatus].color as keyof typeof STATUS_TEXT_COLOR_MAP]} text-sm`, textCls)}>
{DOC_INDEX_STATUS_MAP[localStatus]?.text}
</span>
{
errorMessage && (
<Tooltip
popupContent={
<div className='max-w-[260px] break-all'>{errorMessage}</div>
}
triggerClassName='ml-1 w-4 h-4'
/>
)
}
{
scene === 'detail' && (
<div className='ml-1.5 flex items-center justify-between'>
<Tooltip
popupContent={t('datasetDocuments.list.action.enableWarning')}
popupClassName='text-text-secondary system-xs-medium'
disabled={!archived}
>
<Switch
defaultValue={archived ? false : enabled}
onChange={v => !archived && handleSwitch(v ? 'enable' : 'disable')}
disabled={embedding || archived}
size='md'
/>
</Tooltip>
</div>
)
}
</div>
}

type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_archive' | 'pause' | 'resume'

// operation action for list and detail
export const OperationAction: FC<{
embeddingAvailable: boolean
detail: {
name: string
enabled: boolean
archived: boolean
id: string
data_source_type: string
doc_form: string
display_status?: string
}
datasetId: string
onUpdate: (operationName?: string) => void
scene?: 'list' | 'detail'
className?: string
}> = ({ embeddingAvailable, datasetId, detail, onUpdate, scene = 'list', className = '' }) => {
const downloadDocument = useDocumentDownload()
const { id, enabled = false, archived = false, data_source_type, display_status } = detail || {}
const [showModal, setShowModal] = useState(false)
const [deleting, setDeleting] = useState(false)
const { notify } = useContext(ToastContext)
const { t } = useTranslation()
const router = useRouter()
const { mutateAsync: archiveDocument } = useDocumentArchive()
const { mutateAsync: unArchiveDocument } = useDocumentUnArchive()
const { mutateAsync: enableDocument } = useDocumentEnable()
const { mutateAsync: disableDocument } = useDocumentDisable()
const { mutateAsync: deleteDocument } = useDocumentDelete()
const { mutateAsync: syncDocument } = useSyncDocument()
const { mutateAsync: syncWebsite } = useSyncWebsite()
const { mutateAsync: pauseDocument } = useDocumentPause()
const { mutateAsync: resumeDocument } = useDocumentResume()
const isListScene = scene === 'list'

const onOperate = async (operationName: OperationName) => {
let opApi
switch (operationName) {
case 'archive':
opApi = archiveDocument
break
case 'un_archive':
opApi = unArchiveDocument
break
case 'enable':
opApi = enableDocument
break
case 'disable':
opApi = disableDocument
break
case 'sync':
if (data_source_type === 'notion_import')
opApi = syncDocument
else
opApi = syncWebsite
break
case 'pause':
opApi = pauseDocument
break
case 'resume':
opApi = resumeDocument
break
default:
opApi = deleteDocument
setDeleting(true)
break
}
const [e] = await asyncRunSafe<CommonResponse>(opApi({ datasetId, documentId: id }) as Promise<CommonResponse>)
if (!e) {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
onUpdate(operationName)
}
else { notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) }
if (operationName === 'delete')
setDeleting(false)
}

const { run: handleSwitch } = useDebounceFn((operationName: OperationName) => {
if (operationName === 'enable' && enabled)
return
if (operationName === 'disable' && !enabled)
return
onOperate(operationName)
}, { wait: 500 })

const [currDocument, setCurrDocument] = useState<{
id: string
name: string
} | null>(null)
const [isShowRenameModal, {
setTrue: setShowRenameModalTrue,
setFalse: setShowRenameModalFalse,
}] = useBoolean(false)
const handleShowRenameModal = useCallback((doc: {
id: string
name: string
}) => {
setCurrDocument(doc)
setShowRenameModalTrue()
}, [setShowRenameModalTrue])
const handleRenamed = useCallback(() => {
onUpdate()
}, [onUpdate])

return <div className='flex items-center' onClick={e => e.stopPropagation()}>
{isListScene && !embeddingAvailable && (
<Switch defaultValue={false} onChange={noop} disabled={true} size='md' />
)}
{isListScene && embeddingAvailable && (
<>
{archived
? <Tooltip
popupContent={t('datasetDocuments.list.action.enableWarning')}
popupClassName='!font-semibold'
>
<div>
<Switch defaultValue={false} onChange={noop} disabled={true} size='md' />
</div>
</Tooltip>
: <Switch defaultValue={enabled} onChange={v => handleSwitch(v ? 'enable' : 'disable')} size='md' />
}
<Divider className='!ml-4 !mr-2 !h-3' type='vertical' />
</>
)}
{embeddingAvailable && (
<>
<Tooltip
popupContent={t('datasetDocuments.list.action.download')}
popupClassName='text-text-secondary system-xs-medium'
needsDelay={false}
>
<button
className={cn('mr-2 cursor-pointer rounded-lg',
!isListScene
? 'shadow-shadow-3 border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg p-2 shadow-xs backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
: 'p-0.5 hover:bg-state-base-hover')}
onClick={() => {
downloadDocument.mutateAsync({
datasetId,
documentId: detail.id,
}).then((response) => {
if (response.download_url)
window.location.href = response.download_url
}).catch((error) => {
console.error(error)
notify({ type: 'error', message: t('common.actionMsg.downloadFailed') })
})
}}
>
<RiDownloadLine className='h-4 w-4 text-components-button-secondary-text' />
</button>
</Tooltip>
<Tooltip
popupContent={t('datasetDocuments.list.action.settings')}
popupClassName='text-text-secondary system-xs-medium'
needsDelay={false}
>
<button
className={cn('mr-2 cursor-pointer rounded-lg',
!isListScene
? 'border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px] hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover'
: 'p-0.5 hover:bg-state-base-hover')}
onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
<RiEqualizer2Line className='h-4 w-4 text-components-button-secondary-text' />
</button>
</Tooltip>
<Popover
htmlContent={
<div className='w-full py-1'>
{!archived && (
<>
<div className={s.actionItem} onClick={() => {
handleShowRenameModal({
id: detail.id,
name: detail.name,
})
}}>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.table.rename')}</span>
</div>
{['notion_import', DataSourceType.WEB].includes(data_source_type) && (
<div className={s.actionItem} onClick={() => onOperate('sync')}>
<RiLoopLeftLine className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.action.sync')}</span>
</div>
)}
<Divider className='my-1' />
</>
)}
{!archived && display_status?.toLowerCase() === 'indexing' && (
<div className={s.actionItem} onClick={() => onOperate('pause')}>
<RiPauseCircleLine className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.action.pause')}</span>
</div>
)}
{!archived && display_status?.toLowerCase() === 'paused' && (
<div className={s.actionItem} onClick={() => onOperate('resume')}>
<RiPlayCircleLine className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.action.resume')}</span>
</div>
)}
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
<RiArchive2Line className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
</div>}
{archived && (
<div className={s.actionItem} onClick={() => onOperate('un_archive')}>
<RiArchive2Line className='h-4 w-4 text-text-tertiary' />
<span className={s.actionName}>{t('datasetDocuments.list.action.unarchive')}</span>
</div>
)}
<div className={cn(s.actionItem, s.deleteActionItem, 'group')} onClick={() => setShowModal(true)}>
<RiDeleteBinLine className={'h-4 w-4 text-text-tertiary group-hover:text-text-destructive'} />
<span className={cn(s.actionName, 'group-hover:text-text-destructive')}>{t('datasetDocuments.list.action.delete')}</span>
</div>
</div>
}
trigger='click'
position='br'
btnElement={
<div className={cn(s.commonIcon)}>
<RiMoreFill className='h-4 w-4 text-components-button-secondary-text' />
</div>
}
btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!hover:bg-state-base-hover !shadow-none' : '!bg-transparent')}
popupClassName='!w-full'
className={`!z-20 flex h-fit !w-[200px] justify-end ${className}`}
/>
</>
)}
{showModal
&& <Confirm
isShow={showModal}
isLoading={deleting}
isDisabled={deleting}
title={t('datasetDocuments.list.delete.title')}
content={t('datasetDocuments.list.delete.content')}
confirmText={t('common.operation.sure')}
onConfirm={() => onOperate('delete')}
onCancel={() => setShowModal(false)}
/>
}

{isShowRenameModal && currDocument && (
<RenameModal
datasetId={datasetId}
documentId={currDocument.id}
name={currDocument.name}
onClose={setShowRenameModalFalse}
onSaved={handleRenamed}
/>
)}
</div>
}


export const renderTdValue = (value: string | number | null, isEmptyStyle = false) => { export const renderTdValue = (value: string | number | null, isEmptyStyle = false) => {
return ( return (
const result = aValue.localeCompare(bValue) const result = aValue.localeCompare(bValue)
return sortOrder === 'asc' ? result : -result return sortOrder === 'asc' ? result : -result
} }
else {
else {
const result = aValue - bValue const result = aValue - bValue
return sortOrder === 'asc' ? result : -result return sortOrder === 'asc' ? result : -result
} }
if (sortField === field) { if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc') setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} }
else {
else {
setSortField(field) setSortField(field)
setSortOrder('desc') setSortOrder('desc')
} }

+ 1
- 1
web/app/components/explore/category.tsx Zobrazit soubor

) )


return ( return (
<div className={cn(className, 'flex flex-wrap space-x-1 text-[13px]')}>
<div className={cn(className, 'flex flex-wrap gap-1 text-[13px]')}>
<div <div
className={itemClassName(isAllCategories)} className={itemClassName(isAllCategories)}
onClick={() => onChange(allCategoriesEn)} onClick={() => onChange(allCategoriesEn)}

web/app/components/goto-anything/actions/command-bus.ts → web/app/components/goto-anything/actions/commands/command-bus.ts Zobrazit soubor



const handlers = new Map<string, CommandHandler>() const handlers = new Map<string, CommandHandler>()


export const registerCommand = (name: string, handler: CommandHandler) => {
const registerCommand = (name: string, handler: CommandHandler) => {
handlers.set(name, handler) handlers.set(name, handler)
} }


export const unregisterCommand = (name: string) => {
const unregisterCommand = (name: string) => {
handlers.delete(name) handlers.delete(name)
} }



+ 15
- 0
web/app/components/goto-anything/actions/commands/index.ts Zobrazit soubor

// Command system exports
export { slashAction } from './slash'
export { registerSlashCommands, unregisterSlashCommands, SlashCommandProvider } from './slash'

// Command registry system (for extending with custom commands)
export { slashCommandRegistry, SlashCommandRegistry } from './registry'
export type { SlashCommandHandler } from './types'

// Command bus (for extending with custom commands)
export {
executeCommand,
registerCommands,
unregisterCommands,
type CommandHandler,
} from './command-bus'

+ 53
- 0
web/app/components/goto-anything/actions/commands/language.tsx Zobrazit soubor

import type { SlashCommandHandler } from './types'
import type { CommandSearchResult } from '../types'
import { languages } from '@/i18n-config/language'
import i18n from '@/i18n-config/i18next-config'
import { registerCommands, unregisterCommands } from './command-bus'

// Language dependency types
type LanguageDeps = {
setLocale?: (locale: string) => Promise<void>
}

const buildLanguageCommands = (query: string): CommandSearchResult[] => {
const q = query.toLowerCase()
const list = languages.filter(item => item.supported && (
!q || item.name.toLowerCase().includes(q) || String(item.value).toLowerCase().includes(q)
))
return list.map(item => ({
id: `lang-${item.value}`,
title: item.name,
description: i18n.t('app.gotoAnything.actions.languageChangeDesc'),
type: 'command' as const,
data: { command: 'i18n.set', args: { locale: item.value } },
}))
}

/**
* Language command handler
* Integrates UI building, search, and registration logic
*/
export const languageCommand: SlashCommandHandler<LanguageDeps> = {
name: 'language',
aliases: ['lang'],
description: 'Switch between different languages',

async search(args: string, _locale: string = 'en') {
// Return language options directly, regardless of parameters
return buildLanguageCommands(args)
},

register(deps: LanguageDeps) {
registerCommands({
'i18n.set': async (args) => {
const locale = args?.locale
if (locale)
await deps.setLocale?.(locale)
},
})
},

unregister() {
unregisterCommands(['i18n.set'])
},
}

+ 233
- 0
web/app/components/goto-anything/actions/commands/registry.ts Zobrazit soubor

import type { SlashCommandHandler } from './types'
import type { CommandSearchResult } from '../types'

/**
* Slash Command Registry System
* Responsible for managing registration, lookup, and search of all slash commands
*/
export class SlashCommandRegistry {
private commands = new Map<string, SlashCommandHandler>()
private commandDeps = new Map<string, any>()

/**
* Register command handler
*/
register<TDeps = any>(handler: SlashCommandHandler<TDeps>, deps?: TDeps) {
// Register main command name
this.commands.set(handler.name, handler)

// Register aliases
if (handler.aliases) {
handler.aliases.forEach((alias) => {
this.commands.set(alias, handler)
})
}

// Store dependencies and call registration method
if (deps) {
this.commandDeps.set(handler.name, deps)
handler.register?.(deps)
}
}

/**
* Unregister command
*/
unregister(name: string) {
const handler = this.commands.get(name)
if (handler) {
// Call the command's unregister method
handler.unregister?.()

// Remove dependencies
this.commandDeps.delete(handler.name)

// Remove main command name
this.commands.delete(handler.name)

// Remove all aliases
if (handler.aliases) {
handler.aliases.forEach((alias) => {
this.commands.delete(alias)
})
}
}
}

/**
* Find command handler
*/
findCommand(commandName: string): SlashCommandHandler | undefined {
return this.commands.get(commandName)
}

/**
* Smart partial command matching
* Prioritize alias matching, then match command name prefix
*/
private findBestPartialMatch(partialName: string): SlashCommandHandler | undefined {
const lowerPartial = partialName.toLowerCase()

// First check if any alias starts with this
const aliasMatch = this.findHandlerByAliasPrefix(lowerPartial)
if (aliasMatch)
return aliasMatch

// Then check if command name starts with this
return this.findHandlerByNamePrefix(lowerPartial)
}

/**
* Find handler by alias prefix
*/
private findHandlerByAliasPrefix(prefix: string): SlashCommandHandler | undefined {
for (const handler of this.getAllCommands()) {
if (handler.aliases?.some(alias => alias.toLowerCase().startsWith(prefix)))
return handler
}
return undefined
}

/**
* Find handler by name prefix
*/
private findHandlerByNamePrefix(prefix: string): SlashCommandHandler | undefined {
return this.getAllCommands().find(handler =>
handler.name.toLowerCase().startsWith(prefix),
)
}

/**
* Get all registered commands (deduplicated)
*/
getAllCommands(): SlashCommandHandler[] {
const uniqueCommands = new Map<string, SlashCommandHandler>()
this.commands.forEach((handler) => {
uniqueCommands.set(handler.name, handler)
})
return Array.from(uniqueCommands.values())
}

/**
* Search commands
* @param query Full query (e.g., "/theme dark" or "/lang en")
* @param locale Current language
*/
async search(query: string, locale: string = 'en'): Promise<CommandSearchResult[]> {
const trimmed = query.trim()

// Handle root level search "/"
if (trimmed === '/' || !trimmed.replace('/', '').trim())
return await this.getRootCommands()

// Parse command and arguments
const afterSlash = trimmed.substring(1).trim()
const spaceIndex = afterSlash.indexOf(' ')
const commandName = spaceIndex === -1 ? afterSlash : afterSlash.substring(0, spaceIndex)
const args = spaceIndex === -1 ? '' : afterSlash.substring(spaceIndex + 1).trim()

// First try exact match
let handler = this.findCommand(commandName)
if (handler) {
try {
return await handler.search(args, locale)
}
catch (error) {
console.warn(`Command search failed for ${commandName}:`, error)
return []
}
}

// If no exact match, try smart partial matching
handler = this.findBestPartialMatch(commandName)
if (handler) {
try {
return await handler.search(args, locale)
}
catch (error) {
console.warn(`Command search failed for ${handler.name}:`, error)
return []
}
}

// Finally perform fuzzy search
return this.fuzzySearchCommands(afterSlash)
}

/**
* Get root level command list
*/
private async getRootCommands(): Promise<CommandSearchResult[]> {
const results: CommandSearchResult[] = []

// Generate a root level item for each command
for (const handler of this.getAllCommands()) {
results.push({
id: `root-${handler.name}`,
title: `/${handler.name}`,
description: handler.description,
type: 'command' as const,
data: {
command: `root.${handler.name}`,
args: { name: handler.name },
},
})
}

return results
}

/**
* Fuzzy search commands
*/
private fuzzySearchCommands(query: string): CommandSearchResult[] {
const lowercaseQuery = query.toLowerCase()
const matches: CommandSearchResult[] = []

this.getAllCommands().forEach((handler) => {
// Check if command name matches
if (handler.name.toLowerCase().includes(lowercaseQuery)) {
matches.push({
id: `fuzzy-${handler.name}`,
title: `/${handler.name}`,
description: handler.description,
type: 'command' as const,
data: {
command: `root.${handler.name}`,
args: { name: handler.name },
},
})
}

// Check if aliases match
if (handler.aliases) {
handler.aliases.forEach((alias) => {
if (alias.toLowerCase().includes(lowercaseQuery)) {
matches.push({
id: `fuzzy-${alias}`,
title: `/${alias}`,
description: `${handler.description} (alias for /${handler.name})`,
type: 'command' as const,
data: {
command: `root.${handler.name}`,
args: { name: handler.name },
},
})
}
})
}
})

return matches
}

/**
* Get command dependencies
*/
getCommandDependencies(commandName: string): any {
return this.commandDeps.get(commandName)
}
}

// Global registry instance
export const slashCommandRegistry = new SlashCommandRegistry()

+ 52
- 0
web/app/components/goto-anything/actions/commands/slash.tsx Zobrazit soubor

'use client'
import { useEffect } from 'react'
import type { ActionItem } from '../types'
import { slashCommandRegistry } from './registry'
import { executeCommand } from './command-bus'
import { useTheme } from 'next-themes'
import { setLocaleOnClient } from '@/i18n-config'
import { themeCommand } from './theme'
import { languageCommand } from './language'
import i18n from '@/i18n-config/i18next-config'

export const slashAction: ActionItem = {
key: '/',
shortcut: '/',
title: i18n.t('app.gotoAnything.actions.slashTitle'),
description: i18n.t('app.gotoAnything.actions.slashDesc'),
action: (result) => {
if (result.type !== 'command') return
const { command, args } = result.data
executeCommand(command, args)
},
search: async (query, _searchTerm = '') => {
// Delegate all search logic to the command registry system
return slashCommandRegistry.search(query, i18n.language)
},
}

// Register/unregister default handlers for slash commands with external dependencies.
export const registerSlashCommands = (deps: Record<string, any>) => {
// Register command handlers to the registry system with their respective dependencies
slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme })
slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale })
}

export const unregisterSlashCommands = () => {
// Remove command handlers from registry system (automatically calls each command's unregister method)
slashCommandRegistry.unregister('theme')
slashCommandRegistry.unregister('language')
}

export const SlashCommandProvider = () => {
const theme = useTheme()
useEffect(() => {
registerSlashCommands({
setTheme: theme.setTheme,
setLocale: setLocaleOnClient,
})
return () => unregisterSlashCommands()
}, [theme.setTheme])

return null
}

web/app/components/goto-anything/actions/run-theme.tsx → web/app/components/goto-anything/actions/commands/theme.tsx Zobrazit soubor

import type { CommandSearchResult } from './types'
import type { SlashCommandHandler } from './types'
import type { CommandSearchResult } from '../types'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { RiComputerLine, RiMoonLine, RiPaletteLine, RiSunLine } from '@remixicon/react'
import React from 'react'
import { RiComputerLine, RiMoonLine, RiSunLine } from '@remixicon/react'
import i18n from '@/i18n-config/i18next-config' import i18n from '@/i18n-config/i18next-config'
import { registerCommands, unregisterCommands } from './command-bus'

// Theme dependency types
type ThemeDeps = {
setTheme?: (value: 'light' | 'dark' | 'system') => void
}


const THEME_ITEMS: { id: 'light' | 'dark' | 'system'; titleKey: string; descKey: string; icon: ReactNode }[] = [ const THEME_ITEMS: { id: 'light' | 'dark' | 'system'; titleKey: string; descKey: string; icon: ReactNode }[] = [
{ {
}, },
] ]


export const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => {
const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => {
const q = query.toLowerCase() const q = query.toLowerCase()
const list = THEME_ITEMS.filter(item => const list = THEME_ITEMS.filter(item =>
!q !q
})) }))
} }


export const buildThemeRootItem = (): CommandSearchResult => {
return {
id: 'category-theme',
title: i18n.t('app.gotoAnything.actions.themeCategoryTitle'),
description: i18n.t('app.gotoAnything.actions.themeCategoryDesc'),
type: 'command',
icon: (
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
<RiPaletteLine className='h-4 w-4 text-text-tertiary' />
</div>
),
data: { command: 'nav.search', args: { query: '@run theme ' } },
}
/**
* Theme command handler
* Integrates UI building, search, and registration logic
*/
export const themeCommand: SlashCommandHandler<ThemeDeps> = {
name: 'theme',
description: 'Switch between light and dark themes',

async search(args: string, locale: string = 'en') {
// Return theme options directly, regardless of parameters
return buildThemeCommands(args, locale)
},

register(deps: ThemeDeps) {
registerCommands({
'theme.set': async (args) => {
deps.setTheme?.(args?.value)
},
})
},

unregister() {
unregisterCommands(['theme.set'])
},
} }

+ 33
- 0
web/app/components/goto-anything/actions/commands/types.ts Zobrazit soubor

import type { CommandSearchResult } from '../types'

/**
* Slash command handler interface
* Each slash command should implement this interface
*/
export type SlashCommandHandler<TDeps = any> = {
/** Command name (e.g., 'theme', 'language') */
name: string

/** Command alias list (e.g., ['lang'] for language) */
aliases?: string[]

/** Command description */
description: string

/**
* Search command results
* @param args Command arguments (part after removing command name)
* @param locale Current language
*/
search: (args: string, locale?: string) => Promise<CommandSearchResult[]>

/**
* Called when registering command, passing external dependencies
*/
register?: (deps: TDeps) => void

/**
* Called when unregistering command
*/
unregister?: () => void
}

+ 177
- 5
web/app/components/goto-anything/actions/index.ts Zobrazit soubor

/**
* Goto Anything - Action System
*
* This file defines the action registry for the goto-anything search system.
* Actions handle different types of searches: apps, knowledge bases, plugins, workflow nodes, and commands.
*
* ## How to Add a New Slash Command
*
* 1. **Create Command Handler File** (in `./commands/` directory):
* ```typescript
* // commands/my-command.ts
* import type { SlashCommandHandler } from './types'
* import type { CommandSearchResult } from '../types'
* import { registerCommands, unregisterCommands } from './command-bus'
*
* interface MyCommandDeps {
* myService?: (data: any) => Promise<void>
* }
*
* export const myCommand: SlashCommandHandler<MyCommandDeps> = {
* name: 'mycommand',
* aliases: ['mc'], // Optional aliases
* description: 'My custom command description',
*
* async search(args: string, locale: string = 'en') {
* // Return search results based on args
* return [{
* id: 'my-result',
* title: 'My Command Result',
* description: 'Description of the result',
* type: 'command' as const,
* data: { command: 'my.action', args: { value: args } }
* }]
* },
*
* register(deps: MyCommandDeps) {
* registerCommands({
* 'my.action': async (args) => {
* await deps.myService?.(args?.value)
* }
* })
* },
*
* unregister() {
* unregisterCommands(['my.action'])
* }
* }
* ```
*
* **Example for Self-Contained Command (no external dependencies):**
* ```typescript
* // commands/calculator-command.ts
* export const calculatorCommand: SlashCommandHandler = {
* name: 'calc',
* aliases: ['calculator'],
* description: 'Simple calculator',
*
* async search(args: string) {
* if (!args.trim()) return []
* try {
* // Safe math evaluation (implement proper parser in real use)
* const result = Function('"use strict"; return (' + args + ')')()
* return [{
* id: 'calc-result',
* title: `${args} = ${result}`,
* description: 'Calculator result',
* type: 'command' as const,
* data: { command: 'calc.copy', args: { result: result.toString() } }
* }]
* } catch {
* return [{
* id: 'calc-error',
* title: 'Invalid expression',
* description: 'Please enter a valid math expression',
* type: 'command' as const,
* data: { command: 'calc.noop', args: {} }
* }]
* }
* },
*
* register() {
* registerCommands({
* 'calc.copy': (args) => navigator.clipboard.writeText(args.result),
* 'calc.noop': () => {} // No operation
* })
* },
*
* unregister() {
* unregisterCommands(['calc.copy', 'calc.noop'])
* }
* }
* ```
*
* 2. **Register Command** (in `./commands/slash.tsx`):
* ```typescript
* import { myCommand } from './my-command'
* import { calculatorCommand } from './calculator-command' // For self-contained commands
*
* export const registerSlashCommands = (deps: Record<string, any>) => {
* slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme })
* slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale })
* slashCommandRegistry.register(myCommand, { myService: deps.myService }) // With dependencies
* slashCommandRegistry.register(calculatorCommand) // Self-contained, no dependencies
* }
*
* export const unregisterSlashCommands = () => {
* slashCommandRegistry.unregister('theme')
* slashCommandRegistry.unregister('language')
* slashCommandRegistry.unregister('mycommand')
* slashCommandRegistry.unregister('calc') // Add this line
* }
* ```
*
*
* 3. **Update SlashCommandProvider** (in `./commands/slash.tsx`):
* ```typescript
* export const SlashCommandProvider = () => {
* const theme = useTheme()
* const myService = useMyService() // Add external dependency if needed
*
* useEffect(() => {
* registerSlashCommands({
* setTheme: theme.setTheme, // Required for theme command
* setLocale: setLocaleOnClient, // Required for language command
* myService: myService, // Required for your custom command
* // Note: calculatorCommand doesn't need dependencies, so not listed here
* })
* return () => unregisterSlashCommands()
* }, [theme.setTheme, myService]) // Update dependency array for all dynamic deps
*
* return null
* }
* ```
*
* **Note:** Self-contained commands (like calculator) don't require dependencies but are
* still registered through the same system for consistent lifecycle management.
*
* 4. **Usage**: Users can now type `/mycommand` or `/mc` to use your command
*
* ## Command System Architecture
* - Commands are registered via `SlashCommandRegistry`
* - Each command is self-contained with its own dependencies
* - Commands support aliases for easier access
* - Command execution is handled by the command bus system
* - All commands should be registered through `SlashCommandProvider` for consistent lifecycle management
*
* ## Command Types
* **Commands with External Dependencies:**
* - Require external services, APIs, or React hooks
* - Must provide dependencies in `SlashCommandProvider`
* - Example: theme commands (needs useTheme), API commands (needs service)
*
* **Self-Contained Commands:**
* - Pure logic operations, no external dependencies
* - Still recommended to register through `SlashCommandProvider` for consistency
* - Example: calculator, text manipulation commands
*
* ## Available Actions
* - `@app` - Search applications
* - `@knowledge` / `@kb` - Search knowledge bases
* - `@plugin` - Search plugins
* - `@node` - Search workflow nodes (workflow pages only)
* - `/` - Execute slash commands (theme, language, etc.)
*/

import { appAction } from './app' import { appAction } from './app'
import { knowledgeAction } from './knowledge' import { knowledgeAction } from './knowledge'
import { pluginAction } from './plugin' import { pluginAction } from './plugin'
import { workflowNodesAction } from './workflow-nodes' import { workflowNodesAction } from './workflow-nodes'
import type { ActionItem, SearchResult } from './types' import type { ActionItem, SearchResult } from './types'
import { commandAction } from './run'
import { slashAction } from './commands'


export const Actions = { export const Actions = {
slash: slashAction,
app: appAction, app: appAction,
knowledge: knowledgeAction, knowledge: knowledgeAction,
plugin: pluginAction, plugin: pluginAction,
run: commandAction,
node: workflowNodesAction, node: workflowNodesAction,
} }


} }
} }


if (query.startsWith('@'))
if (query.startsWith('@') || query.startsWith('/'))
return [] return []


const globalSearchActions = Object.values(Actions)

// Use Promise.allSettled to handle partial failures gracefully // Use Promise.allSettled to handle partial failures gracefully
const searchPromises = Object.values(Actions).map(async (action) => {
const searchPromises = globalSearchActions.map(async (action) => {
try { try {
const results = await action.search(query, query, locale) const results = await action.search(query, query, locale)
return { success: true, data: results, actionType: action.key } return { success: true, data: results, actionType: action.key }
allResults.push(...result.value.data) allResults.push(...result.value.data)
} }
else { else {
const actionKey = Object.values(Actions)[index]?.key || 'unknown'
const actionKey = globalSearchActions[index]?.key || 'unknown'
failedActions.push(actionKey) failedActions.push(actionKey)
} }
}) })


export const matchAction = (query: string, actions: Record<string, ActionItem>) => { export const matchAction = (query: string, actions: Record<string, ActionItem>) => {
return Object.values(actions).find((action) => { return Object.values(actions).find((action) => {
// Special handling for slash commands to allow direct /theme, /lang
if (action.key === '/')
return query.startsWith('/')

const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`) const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`)
return reg.test(query) return reg.test(query)
}) })
} }


export * from './types' export * from './types'
export * from './commands'
export { appAction, knowledgeAction, pluginAction, workflowNodesAction } export { appAction, knowledgeAction, pluginAction, workflowNodesAction }

+ 0
- 33
web/app/components/goto-anything/actions/run-language.tsx Zobrazit soubor

import type { CommandSearchResult } from './types'
import { languages } from '@/i18n-config/language'
import { RiTranslate } from '@remixicon/react'
import i18n from '@/i18n-config/i18next-config'

export const buildLanguageCommands = (query: string): CommandSearchResult[] => {
const q = query.toLowerCase()
const list = languages.filter(item => item.supported && (
!q || item.name.toLowerCase().includes(q) || String(item.value).toLowerCase().includes(q)
))
return list.map(item => ({
id: `lang-${item.value}`,
title: item.name,
description: i18n.t('app.gotoAnything.actions.languageChangeDesc'),
type: 'command' as const,
data: { command: 'i18n.set', args: { locale: item.value } },
}))
}

export const buildLanguageRootItem = (): CommandSearchResult => {
return {
id: 'category-language',
title: i18n.t('app.gotoAnything.actions.languageCategoryTitle'),
description: i18n.t('app.gotoAnything.actions.languageCategoryDesc'),
type: 'command',
icon: (
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
<RiTranslate className='h-4 w-4 text-text-tertiary' />
</div>
),
data: { command: 'nav.search', args: { query: '@run language ' } },
}
}

+ 0
- 97
web/app/components/goto-anything/actions/run.tsx Zobrazit soubor

'use client'
import { useEffect } from 'react'
import type { ActionItem, CommandSearchResult } from './types'
import { buildLanguageCommands, buildLanguageRootItem } from './run-language'
import { buildThemeCommands, buildThemeRootItem } from './run-theme'
import i18n from '@/i18n-config/i18next-config'
import { executeCommand, registerCommands, unregisterCommands } from './command-bus'
import { useTheme } from 'next-themes'
import { setLocaleOnClient } from '@/i18n-config'

const rootParser = (query: string): CommandSearchResult[] => {
const q = query.toLowerCase()
const items: CommandSearchResult[] = []
if (!q || 'theme'.includes(q))
items.push(buildThemeRootItem())
if (!q || 'language'.includes(q) || 'lang'.includes(q))
items.push(buildLanguageRootItem())
return items
}

type RunContext = {
setTheme?: (value: 'light' | 'dark' | 'system') => void
setLocale?: (locale: string) => Promise<void>
search?: (query: string) => void
}

export const commandAction: ActionItem = {
key: '@run',
shortcut: '@run',
title: i18n.t('app.gotoAnything.actions.runTitle'),
description: i18n.t('app.gotoAnything.actions.runDesc'),
action: (result) => {
if (result.type !== 'command') return
const { command, args } = result.data
if (command === 'theme.set') {
executeCommand('theme.set', args)
return
}
if (command === 'i18n.set') {
executeCommand('i18n.set', args)
return
}
if (command === 'nav.search')
executeCommand('nav.search', args)
},
search: async (_, searchTerm = '') => {
const q = searchTerm.trim()
if (q.startsWith('theme'))
return buildThemeCommands(q.replace(/^theme\s*/, ''), i18n.language)
if (q.startsWith('language') || q.startsWith('lang'))
return buildLanguageCommands(q.replace(/^(language|lang)\s*/, ''))

// root categories
return rootParser(q)
},
}

// Register/unregister default handlers for @run commands with external dependencies.
export const registerRunCommands = (deps: {
setTheme?: (value: 'light' | 'dark' | 'system') => void
setLocale?: (locale: string) => Promise<void>
search?: (query: string) => void
}) => {
registerCommands({
'theme.set': async (args) => {
deps.setTheme?.(args?.value)
},
'i18n.set': async (args) => {
const locale = args?.locale
if (locale)
await deps.setLocale?.(locale)
},
'nav.search': (args) => {
const q = args?.query
if (q)
deps.search?.(q)
},
})
}

export const unregisterRunCommands = () => {
unregisterCommands(['theme.set', 'i18n.set', 'nav.search'])
}

export const RunCommandProvider = ({ onNavSearch }: { onNavSearch?: (q: string) => void }) => {
const theme = useTheme()
useEffect(() => {
registerRunCommands({
setTheme: theme.setTheme,
setLocale: setLocaleOnClient,
search: onNavSearch,
})
return () => unregisterRunCommands()
}, [theme.setTheme, onNavSearch])

return null
}

+ 1
- 1
web/app/components/goto-anything/actions/types.ts Zobrazit soubor

export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult | CommandSearchResult export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult | CommandSearchResult


export type ActionItem = { export type ActionItem = {
key: '@app' | '@knowledge' | '@plugin' | '@node' | '@run'
key: '@app' | '@knowledge' | '@plugin' | '@node' | '/'
shortcut: string shortcut: string
title: string | TypeWithI18N title: string | TypeWithI18N
description: string description: string

+ 1
- 1
web/app/components/goto-anything/command-selector.tsx Zobrazit soubor

<span className="ml-3 text-sm text-text-secondary"> <span className="ml-3 text-sm text-text-secondary">
{(() => { {(() => {
const keyMap: Record<string, string> = { const keyMap: Record<string, string> = {
'/': 'app.gotoAnything.actions.slashDesc',
'@app': 'app.gotoAnything.actions.searchApplicationsDesc', '@app': 'app.gotoAnything.actions.searchApplicationsDesc',
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc', '@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
'@run': 'app.gotoAnything.actions.runDesc',
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
} }
return t(keyMap[action.key]) return t(keyMap[action.key])

+ 16
- 16
web/app/components/goto-anything/index.tsx Zobrazit soubor

import type { Plugin } from '../plugins/types' import type { Plugin } from '../plugins/types'
import { Command } from 'cmdk' import { Command } from 'cmdk'
import CommandSelector from './command-selector' import CommandSelector from './command-selector'
import { RunCommandProvider } from './actions/run'
import { SlashCommandProvider } from './actions/commands'


type Props = { type Props = {
onHide?: () => void onHide?: () => void
const [searchQuery, setSearchQuery] = useState<string>('') const [searchQuery, setSearchQuery] = useState<string>('')
const [cmdVal, setCmdVal] = useState<string>('_') const [cmdVal, setCmdVal] = useState<string>('_')
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const handleNavSearch = useCallback((q: string) => {
setShow(true)
setSearchQuery(q)
setCmdVal('')
requestAnimationFrame(() => inputRef.current?.focus())
}, [])

// Filter actions based on context // Filter actions based on context
const Actions = useMemo(() => { const Actions = useMemo(() => {
// Create a filtered copy of actions based on current page context // Create a filtered copy of actions based on current page context
return AllActions return AllActions
} }
else { else {
// Exclude node action on non-workflow pages
const { app, knowledge, plugin, run } = AllActions
return { app, knowledge, plugin, run }
const { app, knowledge, plugin, slash } = AllActions
return { app, knowledge, plugin, slash }
} }
}, [isWorkflowPage]) }, [isWorkflowPage])


wait: 300, wait: 300,
}) })


const isCommandsMode = searchQuery.trim() === '@' || (searchQuery.trim().startsWith('@') && !matchAction(searchQuery.trim(), Actions))
const isCommandsMode = searchQuery.trim() === '@' || searchQuery.trim() === '/'
|| (searchQuery.trim().startsWith('@') && !matchAction(searchQuery.trim(), Actions))
|| (searchQuery.trim().startsWith('/') && !matchAction(searchQuery.trim(), Actions))


const searchMode = useMemo(() => { const searchMode = useMemo(() => {
if (isCommandsMode) return 'commands' if (isCommandsMode) return 'commands'


const query = searchQueryDebouncedValue.toLowerCase() const query = searchQueryDebouncedValue.toLowerCase()
const action = matchAction(query, Actions) const action = matchAction(query, Actions)
return action ? action.key : 'general'
return action
? (action.key === '/' ? '@command' : action.key)
: 'general'
}, [searchQueryDebouncedValue, Actions, isCommandsMode]) }, [searchQueryDebouncedValue, Actions, isCommandsMode])


const { data: searchResults = [], isLoading, isError, error } = useQuery( const { data: searchResults = [], isLoading, isError, error } = useQuery(


switch (result.type) { switch (result.type) {
case 'command': { case 'command': {
const action = Object.values(Actions).find(a => a.key === '@run')
// Execute slash commands
const action = Actions.slash
action?.action?.(result) action?.action?.(result)
break break
} }
</div> </div>
<div className='mt-1 text-xs text-text-quaternary'> <div className='mt-1 text-xs text-text-quaternary'>
{isCommandSearch {isCommandSearch
? t('app.gotoAnything.emptyState.tryDifferentTerm', { mode: searchMode })
? t('app.gotoAnything.emptyState.tryDifferentTerm')
: t('app.gotoAnything.emptyState.trySpecificSearch', { shortcuts: Object.values(Actions).map(action => action.shortcut).join(', ') }) : t('app.gotoAnything.emptyState.trySpecificSearch', { shortcuts: Object.values(Actions).map(action => action.shortcut).join(', ') })
} }
</div> </div>


return ( return (
<> <>
<SlashCommandProvider />
<Modal <Modal
isShow={show} isShow={show}
onClose={() => { onClose={() => {
placeholder={t('app.gotoAnything.searchPlaceholder')} placeholder={t('app.gotoAnything.searchPlaceholder')}
onChange={(e) => { onChange={(e) => {
setSearchQuery(e.target.value) setSearchQuery(e.target.value)
if (!e.target.value.startsWith('@'))
if (!e.target.value.startsWith('@') && !e.target.value.startsWith('/'))
clearSelection() clearSelection()
}} }}
className='flex-1 !border-0 !bg-transparent !shadow-none' className='flex-1 !border-0 !bg-transparent !shadow-none'
'plugin': 'app.gotoAnything.groups.plugins', 'plugin': 'app.gotoAnything.groups.plugins',
'knowledge': 'app.gotoAnything.groups.knowledgeBases', 'knowledge': 'app.gotoAnything.groups.knowledgeBases',
'workflow-node': 'app.gotoAnything.groups.workflowNodes', 'workflow-node': 'app.gotoAnything.groups.workflowNodes',
'command': 'app.gotoAnything.groups.commands',
} }
return t(typeMap[type] || `${type}s`) return t(typeMap[type] || `${type}s`)
})()} className='p-2 capitalize text-text-secondary'> })()} className='p-2 capitalize text-text-secondary'>
</div> </div>


</Modal> </Modal>
<RunCommandProvider onNavSearch={handleNavSearch} />
{ {
activePlugin && ( activePlugin && (
<InstallFromMarketplace <InstallFromMarketplace

+ 1
- 1
web/app/components/header/account-dropdown/support.tsx Zobrazit soubor

> >
<MenuItems <MenuItems
className={cn( className={cn(
`absolute top-[1px] z-10 max-h-[70vh] w-[216px] origin-top-right -translate-x-full divide-y divide-divider-subtle overflow-y-scroll
`absolute top-[1px] z-10 max-h-[70vh] w-[216px] origin-top-right -translate-x-full divide-y divide-divider-subtle overflow-y-auto
rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px] focus:outline-none rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px] focus:outline-none
`, `,
)} )}

+ 4
- 0
web/app/components/plugins/plugin-detail-panel/detail-header.tsx Zobrazit soubor

import useReferenceSetting from '../plugin-page/use-reference-setting' import useReferenceSetting from '../plugin-page/use-reference-setting'
import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types' import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'


const i18nPrefix = 'plugin.action' const i18nPrefix = 'plugin.action'


const { setShowUpdatePluginModal } = useModalContext() const { setShowUpdatePluginModal } = useModalContext()
const { refreshModelProviders } = useProviderContext() const { refreshModelProviders } = useProviderContext()
const invalidateAllToolProviders = useInvalidateAllToolProviders() const invalidateAllToolProviders = useInvalidateAllToolProviders()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)


const { const {
installation_id, installation_id,
const { referenceSetting } = useReferenceSetting() const { referenceSetting } = useReferenceSetting()
const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {} const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {}
const isAutoUpgradeEnabled = useMemo(() => { const isAutoUpgradeEnabled = useMemo(() => {
if (!enable_marketplace)
return false
if (!autoUpgradeInfo || !isFromMarketplace) if (!autoUpgradeInfo || !isFromMarketplace)
return false return false
if(autoUpgradeInfo.strategy_setting === 'disabled') if(autoUpgradeInfo.strategy_setting === 'disabled')

+ 1
- 1
web/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form.tsx Zobrazit soubor

className='h-8 grow' className='h-8 grow'
type='number' type='number'
value={varInput?.value || ''} value={varInput?.value || ''}
onChange={handleValueChange(variable, type)}
onChange={e => handleValueChange(variable, type)(e.target.value)}
placeholder={placeholder?.[language] || placeholder?.en_US} placeholder={placeholder?.[language] || placeholder?.en_US}
/> />
)} )}

+ 7
- 2
web/app/components/plugins/reference-setting-modal/modal.tsx Zobrazit soubor

import type { AutoUpdateConfig } from './auto-update-setting/types' import type { AutoUpdateConfig } from './auto-update-setting/types'
import AutoUpdateSetting from './auto-update-setting' import AutoUpdateSetting from './auto-update-setting'
import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config' import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
import Label from './label' import Label from './label'


const i18nPrefix = 'plugin.privilege' const i18nPrefix = 'plugin.privilege'
const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {} const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {}
const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege) const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege)
const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue) const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const handlePrivilegeChange = useCallback((key: string) => { const handlePrivilegeChange = useCallback((key: string) => {
return (value: PermissionType) => { return (value: PermissionType) => {
setTempPrivilege({ setTempPrivilege({
</div> </div>
))} ))}
</div> </div>

<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
{
enable_marketplace && (
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
)
}
<div className='flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5'> <div className='flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5'>
<Button <Button
className='min-w-[72px]' className='min-w-[72px]'

+ 18
- 2
web/app/components/workflow-app/components/workflow-header/features-trigger.tsx Zobrazit soubor

useCallback, useCallback,
useMemo, useMemo,
} from 'react' } from 'react'
import { useStore as useReactflowStore } from 'reactflow'
import { useEdges, useNodes, useStore as useReactflowStore } from 'reactflow'
import { RiApps2AddLine } from '@remixicon/react' import { RiApps2AddLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
useWorkflowStore, useWorkflowStore,
} from '@/app/components/workflow/store' } from '@/app/components/workflow/store'
import { import {
useChecklist,
useChecklistBeforePublish, useChecklistBeforePublish,
useNodesReadOnly, useNodesReadOnly,
useNodesSyncDraft, useNodesSyncDraft,
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import AppPublisher from '@/app/components/app/app-publisher' import AppPublisher from '@/app/components/app/app-publisher'
import { useFeatures } from '@/app/components/base/features/hooks' import { useFeatures } from '@/app/components/base/features/hooks'
import type {
CommonEdgeType,
CommonNodeType,
} from '@/app/components/workflow/types'
import { import {
BlockEnum, BlockEnum,
InputVarType, InputVarType,
} }
}, [appID, setAppDetail]) }, [appID, setAppDetail])
const { mutateAsync: publishWorkflow } = usePublishWorkflow() const { mutateAsync: publishWorkflow } = usePublishWorkflow()
const nodes = useNodes<CommonNodeType>()
const edges = useEdges<CommonEdgeType>()
const needWarningNodes = useChecklist(nodes, edges)

const updatePublishedWorkflow = useInvalidateAppWorkflow() const updatePublishedWorkflow = useInvalidateAppWorkflow()
const onPublish = useCallback(async (params?: PublishWorkflowParams) => { const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
// First check if there are any items in the checklist
if (needWarningNodes.length > 0) {
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
throw new Error('Checklist has unresolved items')
}

// Then perform the detailed validation
if (await handleCheckBeforePublish()) { if (await handleCheckBeforePublish()) {
const res = await publishWorkflow({ const res = await publishWorkflow({
url: `/apps/${appID}/workflows/publish`, url: `/apps/${appID}/workflows/publish`,
else { else {
throw new Error('Checklist failed') throw new Error('Checklist failed')
} }
}, [handleCheckBeforePublish, publishWorkflow, notify, appID, t, updatePublishedWorkflow, appID, updateAppDetail, workflowStore, resetWorkflowVersionHistory])
}, [needWarningNodes, handleCheckBeforePublish, publishWorkflow, notify, appID, t, updatePublishedWorkflow, updateAppDetail, workflowStore, resetWorkflowVersionHistory])


const onPublisherToggle = useCallback((state: boolean) => { const onPublisherToggle = useCallback((state: boolean) => {
if (state) if (state)

+ 2
- 1
web/app/components/workflow/hooks/use-workflow-variables.ts Zobrazit soubor

} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import { useIsChatMode } from './use-workflow' import { useIsChatMode } from './use-workflow'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import type { Type } from '../nodes/llm/types'


export const useWorkflowVariables = () => { export const useWorkflowVariables = () => {
const { t } = useTranslation() const { t } = useTranslation()
isChatMode, isChatMode,
isConstant: false, isConstant: false,
}) })
return type
return type as unknown as Type
} }


return getVarType return getVarType

+ 1
- 0
web/app/components/workflow/nodes/_base/components/agent-strategy.tsx Zobrazit soubor

headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase' headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase'
containerBackgroundClassName='bg-transparent' containerBackgroundClassName='bg-transparent'
gradientBorder={false} gradientBorder={false}
nodeId={nodeId}
isSupportPromptGenerator={!!def.auto_generate?.type} isSupportPromptGenerator={!!def.auto_generate?.type}
titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)} titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)}
editorContainerClassName='px-0' editorContainerClassName='px-0'

+ 13
- 3
web/app/components/workflow/nodes/_base/components/code-generator-button.tsx Zobrazit soubor

import { Generator } from '@/app/components/base/icons/src/vender/other' import { Generator } from '@/app/components/base/icons/src/vender/other'
import { ActionButton } from '@/app/components/base/action-button' import { ActionButton } from '@/app/components/base/action-button'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
import type { CodeGenRes } from '@/service/debug'
import type { GenRes } from '@/service/debug'
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res' import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
import { useHooksStore } from '../../../hooks-store'


type Props = { type Props = {
nodeId: string
currentCode?: string
className?: string className?: string
onGenerated?: (prompt: string) => void onGenerated?: (prompt: string) => void
codeLanguages: CodeLanguage codeLanguages: CodeLanguage
} }


const CodeGenerateBtn: FC<Props> = ({ const CodeGenerateBtn: FC<Props> = ({
nodeId,
currentCode,
className, className,
codeLanguages, codeLanguages,
onGenerated, onGenerated,
}) => { }) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false) const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = useCallback((res: CodeGenRes) => {
onGenerated?.(res.code)
const handleAutomaticRes = useCallback((res: GenRes) => {
onGenerated?.(res.modified)
showAutomaticFalse() showAutomaticFalse()
}, [onGenerated, showAutomaticFalse]) }, [onGenerated, showAutomaticFalse])
const configsMap = useHooksStore(s => s.configsMap)

return ( return (
<div className={cn(className)}> <div className={cn(className)}>
<ActionButton <ActionButton
codeLanguages={codeLanguages} codeLanguages={codeLanguages}
onClose={showAutomaticFalse} onClose={showAutomaticFalse}
onFinished={handleAutomaticRes} onFinished={handleAutomaticRes}
flowId={configsMap?.flowId || ''}
nodeId={nodeId}
currentCode={currentCode}
/> />
)} )}
</div> </div>

+ 0
- 0
web/app/components/workflow/nodes/_base/components/editor/base.tsx Zobrazit soubor


Některé soubory nejsou zobrazny, neboť je v této revizi změněno mnoho souborů

Načítá se…
Zrušit
Uložit