| @@ -23,6 +23,9 @@ jobs: | |||
| uv run ruff check --fix-only . | |||
| # Format code | |||
| 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 | |||
| @@ -197,6 +197,8 @@ sdks/python-client/dify_client.egg-info | |||
| !.vscode/README.md | |||
| pyrightconfig.json | |||
| api/.vscode | |||
| # vscode Code History Extension | |||
| .history | |||
| .idea/ | |||
| @@ -478,6 +478,13 @@ API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node | |||
| # API workflow run repository implementation | |||
| 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_MAX_EXECUTION_TIME=1200 | |||
| @@ -968,6 +968,14 @@ class AccountConfig(BaseSettings): | |||
| ) | |||
| 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( | |||
| # place the configs in alphabet order | |||
| AppExecutionConfig, | |||
| @@ -1003,5 +1011,6 @@ class FeatureConfig( | |||
| HostedServiceConfig, | |||
| CeleryBeatConfig, | |||
| CeleryScheduleTasksConfig, | |||
| WorkflowLogConfig, | |||
| ): | |||
| pass | |||
| @@ -1,3 +1,4 @@ | |||
| import contextlib | |||
| import mimetypes | |||
| import os | |||
| import platform | |||
| @@ -65,10 +66,8 @@ def guess_file_info_from_response(response: httpx.Response): | |||
| # Use python-magic to guess MIME type if still unknown or generic | |||
| 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) | |||
| except magic.MagicException: | |||
| pass | |||
| extension = os.path.splitext(filename)[1] | |||
| @@ -1,3 +1,5 @@ | |||
| from typing import Literal | |||
| from flask import request | |||
| from flask_login import current_user | |||
| from flask_restful import Resource, marshal, marshal_with, reqparse | |||
| @@ -24,7 +26,7 @@ class AnnotationReplyActionApi(Resource): | |||
| @login_required | |||
| @account_initialization_required | |||
| @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: | |||
| raise Forbidden() | |||
| @@ -38,8 +40,6 @@ class AnnotationReplyActionApi(Resource): | |||
| result = AppAnnotationService.enable_app_annotation(args, app_id) | |||
| elif action == "disable": | |||
| result = AppAnnotationService.disable_app_annotation(app_id) | |||
| else: | |||
| raise ValueError("Unsupported annotation reply action") | |||
| return result, 200 | |||
| @@ -1,3 +1,5 @@ | |||
| from collections.abc import Sequence | |||
| from flask_login import current_user | |||
| from flask_restful import Resource, reqparse | |||
| @@ -10,6 +12,8 @@ from controllers.console.app.error import ( | |||
| ) | |||
| from controllers.console.wraps import account_initialization_required, setup_required | |||
| 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.model_runtime.errors.invoke import InvokeError | |||
| from libs.login import login_required | |||
| @@ -107,6 +111,121 @@ class RuleStructuredOutputGenerateApi(Resource): | |||
| 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(RuleCodeGenerateApi, "/rule-code-generate") | |||
| api.add_resource(RuleStructuredOutputGenerateApi, "/rule-structured-output-generate") | |||
| api.add_resource(InstructionGenerateApi, "/instruction-generate") | |||
| api.add_resource(InstructionGenerationTemplateApi, "/instruction-generate/template") | |||
| @@ -1,7 +1,7 @@ | |||
| import json | |||
| import logging | |||
| from argparse import ArgumentTypeError | |||
| from typing import cast | |||
| from typing import Literal, cast | |||
| from flask import request | |||
| from flask_login import current_user | |||
| @@ -761,7 +761,7 @@ class DocumentProcessingApi(DocumentResource): | |||
| @login_required | |||
| @account_initialization_required | |||
| @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) | |||
| document_id = str(document_id) | |||
| document = self.get_document(dataset_id, document_id) | |||
| @@ -787,8 +787,6 @@ class DocumentProcessingApi(DocumentResource): | |||
| document.paused_at = None | |||
| document.is_paused = False | |||
| db.session.commit() | |||
| else: | |||
| raise InvalidActionError() | |||
| return {"result": "success"}, 200 | |||
| @@ -843,7 +841,7 @@ class DocumentStatusApi(DocumentResource): | |||
| @account_initialization_required | |||
| @cloud_edition_billing_resource_check("vector_space") | |||
| @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 = DatasetService.get_dataset(dataset_id) | |||
| if dataset is None: | |||
| @@ -1,3 +1,5 @@ | |||
| from typing import Literal | |||
| from flask_login import current_user | |||
| from flask_restful import Resource, marshal_with, reqparse | |||
| from werkzeug.exceptions import NotFound | |||
| @@ -100,7 +102,7 @@ class DatasetMetadataBuiltInFieldActionApi(Resource): | |||
| @login_required | |||
| @account_initialization_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 = DatasetService.get_dataset(dataset_id_str) | |||
| if dataset is None: | |||
| @@ -39,7 +39,7 @@ class UploadFileApi(Resource): | |||
| data_source_info = document.data_source_info_dict | |||
| if data_source_info and "upload_file_id" in data_source_info: | |||
| 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: | |||
| raise NotFound("UploadFile not found.") | |||
| else: | |||
| @@ -1,3 +1,5 @@ | |||
| from typing import Literal | |||
| from flask import request | |||
| from flask_restful import Resource, marshal, marshal_with, reqparse | |||
| from werkzeug.exceptions import Forbidden | |||
| @@ -15,7 +17,7 @@ from services.annotation_service import AppAnnotationService | |||
| class AnnotationReplyActionApi(Resource): | |||
| @validate_app_token | |||
| def post(self, app_model: App, action): | |||
| def post(self, app_model: App, action: Literal["enable", "disable"]): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("score_threshold", required=True, type=float, location="json") | |||
| parser.add_argument("embedding_provider_name", required=True, type=str, location="json") | |||
| @@ -25,8 +27,6 @@ class AnnotationReplyActionApi(Resource): | |||
| result = AppAnnotationService.enable_app_annotation(args, app_model.id) | |||
| elif action == "disable": | |||
| result = AppAnnotationService.disable_app_annotation(app_model.id) | |||
| else: | |||
| raise ValueError("Unsupported annotation reply action") | |||
| return result, 200 | |||
| @@ -1,3 +1,5 @@ | |||
| from typing import Literal | |||
| from flask import request | |||
| from flask_restful import marshal, marshal_with, reqparse | |||
| from werkzeug.exceptions import Forbidden, NotFound | |||
| @@ -358,14 +360,14 @@ class DatasetApi(DatasetApiResource): | |||
| class DocumentStatusApi(DatasetApiResource): | |||
| """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. | |||
| Args: | |||
| tenant_id: tenant 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: | |||
| dict: A dictionary with a key 'result' and a value 'success' | |||
| @@ -1,3 +1,5 @@ | |||
| from typing import Literal | |||
| from flask_login import current_user # type: ignore | |||
| from flask_restful import marshal, reqparse | |||
| from werkzeug.exceptions import NotFound | |||
| @@ -77,7 +79,7 @@ class DatasetMetadataBuiltInFieldServiceApi(DatasetApiResource): | |||
| class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource): | |||
| @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 = DatasetService.get_dataset(dataset_id_str) | |||
| if dataset is None: | |||
| @@ -181,7 +181,7 @@ class MessageCycleManager: | |||
| :param message_id: message id | |||
| :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 | |||
| return MessageStreamResponse( | |||
| @@ -1,6 +1,7 @@ | |||
| import json | |||
| import logging | |||
| import re | |||
| from collections.abc import Sequence | |||
| from typing import Optional, cast | |||
| import json_repair | |||
| @@ -11,6 +12,8 @@ from core.llm_generator.prompts import ( | |||
| CONVERSATION_TITLE_PROMPT, | |||
| GENERATOR_QA_PROMPT, | |||
| JAVASCRIPT_CODE_GENERATOR_PROMPT_TEMPLATE, | |||
| LLM_MODIFY_CODE_SYSTEM, | |||
| LLM_MODIFY_PROMPT_SYSTEM, | |||
| PYTHON_CODE_GENERATOR_PROMPT_TEMPLATE, | |||
| SYSTEM_STRUCTURED_OUTPUT_GENERATE, | |||
| WORKFLOW_RULE_CONFIG_PROMPT_GENERATE_TEMPLATE, | |||
| @@ -24,6 +27,9 @@ from core.ops.entities.trace_entity import TraceTaskName | |||
| from core.ops.ops_trace_manager import TraceQueueManager, TraceTask | |||
| from core.ops.utils import measure_time | |||
| 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: | |||
| @@ -388,3 +394,181 @@ class LLMGenerator: | |||
| except Exception as e: | |||
| logging.exception("Failed to invoke LLM model, model: %s", model_config.get("name")) | |||
| 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)}"} | |||
| @@ -309,3 +309,116 @@ eg: | |||
| Here is the JSON schema: | |||
| {{schema}} | |||
| """ # 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#}}.""" | |||
| @@ -4,8 +4,8 @@ import math | |||
| from typing import Any | |||
| 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 configs import dify_config | |||
| @@ -119,14 +119,21 @@ class OceanBaseVector(BaseVector): | |||
| ) | |||
| try: | |||
| 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: | |||
| raise Exception( | |||
| "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", | |||
| e, | |||
| ) | |||
| self._client.refresh_metadata([self._collection_name]) | |||
| redis_client.set(collection_exist_cache_key, 1, ex=3600) | |||
| def _check_hybrid_search_support(self) -> bool: | |||
| @@ -252,7 +259,7 @@ class OceanBaseVector(BaseVector): | |||
| vec_column_name="vector", | |||
| vec_data=query_vector, | |||
| topk=topk, | |||
| distance_func=func.l2_distance, | |||
| distance_func=l2_distance, | |||
| output_column_names=["text", "metadata"], | |||
| with_dist=True, | |||
| where_clause=_where_clause, | |||
| @@ -331,6 +331,12 @@ class QdrantVector(BaseVector): | |||
| def search_by_vector(self, query_vector: list[float], **kwargs: Any) -> list[Document]: | |||
| 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( | |||
| must=[ | |||
| models.FieldCondition( | |||
| @@ -355,7 +361,7 @@ class QdrantVector(BaseVector): | |||
| limit=kwargs.get("top_k", 4), | |||
| with_payload=True, | |||
| with_vectors=True, | |||
| score_threshold=float(kwargs.get("score_threshold") or 0.0), | |||
| score_threshold=score_threshold, | |||
| ) | |||
| docs = [] | |||
| for result in results: | |||
| @@ -363,7 +369,6 @@ class QdrantVector(BaseVector): | |||
| continue | |||
| metadata = result.payload.get(Field.METADATA_KEY.value) or {} | |||
| # duplicate check score threshold | |||
| score_threshold = float(kwargs.get("score_threshold") or 0.0) | |||
| if result.score > score_threshold: | |||
| metadata["score"] = result.score | |||
| doc = Document( | |||
| @@ -145,13 +145,19 @@ def init_app(app: DifyApp) -> Celery: | |||
| 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") | |||
| beat_schedule["check_upgradable_plugin_task"] = { | |||
| "task": "schedule.check_upgradable_plugin_task.check_upgradable_plugin_task", | |||
| "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) | |||
| return celery_app | |||
| @@ -205,7 +205,7 @@ vdb = [ | |||
| "pgvector==0.2.5", | |||
| "pymilvus~=2.5.0", | |||
| "pymochow==1.3.1", | |||
| "pyobvector~=0.1.6", | |||
| "pyobvector~=0.2.15", | |||
| "qdrant-client==1.9.0", | |||
| "tablestore==6.2.0", | |||
| "tcvectordb~=1.6.4", | |||
| @@ -0,0 +1,155 @@ | |||
| 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 | |||
| @@ -293,7 +293,7 @@ class AppAnnotationService: | |||
| annotation_ids_to_delete = [annotation.id for annotation, _ in annotations_to_delete] | |||
| # 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) | |||
| ).delete(synchronize_session=False) | |||
| @@ -307,7 +307,7 @@ class AppAnnotationService: | |||
| # Step 4: Bulk delete annotations in a single query | |||
| deleted_count = ( | |||
| db.session.query(MessageAnnotation) | |||
| .filter(MessageAnnotation.id.in_(annotation_ids_to_delete)) | |||
| .where(MessageAnnotation.id.in_(annotation_ids_to_delete)) | |||
| .delete(synchronize_session=False) | |||
| ) | |||
| @@ -505,9 +505,9 @@ class AppAnnotationService: | |||
| 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): | |||
| annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).filter( | |||
| annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).where( | |||
| AppAnnotationHitHistory.annotation_id == annotation.id | |||
| ) | |||
| for annotation_hit_history in annotation_hit_histories_query.yield_per(100): | |||
| @@ -6,7 +6,7 @@ import secrets | |||
| import time | |||
| import uuid | |||
| from collections import Counter | |||
| from typing import Any, Optional | |||
| from typing import Any, Literal, Optional | |||
| from flask_login import current_user | |||
| from sqlalchemy import func, select | |||
| @@ -55,7 +55,7 @@ from services.entities.knowledge_entities.rag_pipeline_entities import ( | |||
| KnowledgeConfiguration, | |||
| RagPipelineDatasetCreateEntity, | |||
| ) | |||
| from services.errors.account import InvalidActionError, NoPermissionError | |||
| from services.errors.account import NoPermissionError | |||
| from services.errors.chunk import ChildChunkDeleteIndexError, ChildChunkIndexingError | |||
| from services.errors.dataset import DatasetNameDuplicateError | |||
| from services.errors.document import DocumentIndexingError | |||
| @@ -2231,14 +2231,16 @@ class DocumentService: | |||
| raise ValueError("Process rule segmentation max_tokens is invalid") | |||
| @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. | |||
| Args: | |||
| dataset (Dataset): The dataset object | |||
| 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 | |||
| Raises: | |||
| @@ -2321,9 +2323,10 @@ class DocumentService: | |||
| raise propagation_error | |||
| @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: | |||
| document: Document object to update | |||
| @@ -2786,7 +2789,9 @@ class SegmentService: | |||
| db.session.commit() | |||
| @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 | |||
| if not segment_ids or len(segment_ids) == 0: | |||
| return | |||
| @@ -2844,8 +2849,6 @@ class SegmentService: | |||
| db.session.commit() | |||
| disable_segments_from_index_task.delay(real_deal_segment_ids, dataset.id, document.id) | |||
| else: | |||
| raise InvalidActionError() | |||
| @classmethod | |||
| def create_child_chunk( | |||
| @@ -1,5 +1,6 @@ | |||
| import logging | |||
| import time | |||
| from typing import Literal | |||
| import click | |||
| from celery import shared_task # type: ignore | |||
| @@ -13,7 +14,7 @@ from models.dataset import Document as DatasetDocument | |||
| @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 | |||
| :param dataset_id: dataset_id | |||
| @@ -1,4 +1,5 @@ | |||
| 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 ( | |||
| AbstractVectorTest, | |||
| setup_mock_redis, | |||
| @@ -18,6 +19,14 @@ class QdrantVectorTest(AbstractVectorTest): | |||
| ), | |||
| ) | |||
| 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): | |||
| QdrantVectorTest().run_all_tests() | |||
| @@ -471,7 +471,7 @@ class TestAnnotationService: | |||
| # Verify annotation was deleted | |||
| 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 | |||
| # Verify delete_annotation_index_task was called (when annotation setting exists) | |||
| @@ -1175,7 +1175,7 @@ class TestAnnotationService: | |||
| AppAnnotationService.delete_app_annotation(app.id, annotation_id) | |||
| # 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 | |||
| # Verify delete_annotation_index_task was called | |||
| @@ -234,7 +234,7 @@ class TestAPIBasedExtensionService: | |||
| # Verify extension was deleted | |||
| 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 | |||
| def test_save_extension_duplicate_name(self, db_session_with_containers, mock_external_service_dependencies): | |||
| @@ -484,7 +484,7 @@ class TestMessageService: | |||
| # Verify feedback was deleted | |||
| 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 | |||
| def test_create_feedback_no_rating_when_not_exists( | |||
| @@ -469,6 +469,6 @@ class TestModelLoadBalancingService: | |||
| # Verify inherit config was created in database | |||
| 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 | |||
| @@ -1602,7 +1602,7 @@ vdb = [ | |||
| { name = "pgvector", specifier = "==0.2.5" }, | |||
| { name = "pymilvus", specifier = "~=2.5.0" }, | |||
| { 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 = "tablestore", specifier = "==6.2.0" }, | |||
| { name = "tcvectordb", specifier = "~=1.6.4" }, | |||
| @@ -4569,17 +4569,19 @@ wheels = [ | |||
| [[package]] | |||
| name = "pyobvector" | |||
| version = "0.1.14" | |||
| version = "0.2.15" | |||
| source = { registry = "https://pypi.org/simple" } | |||
| dependencies = [ | |||
| { name = "aiomysql" }, | |||
| { name = "numpy" }, | |||
| { name = "pydantic" }, | |||
| { name = "pymysql" }, | |||
| { 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 = [ | |||
| { 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]] | |||
| @@ -5432,6 +5434,15 @@ wheels = [ | |||
| { 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]] | |||
| name = "sseclient-py" | |||
| version = "1.8.0" | |||
| @@ -887,6 +887,14 @@ API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository. | |||
| # API workflow node execution repository implementation | |||
| 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_MAX_BINARY_SIZE=10485760 | |||
| HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576 | |||
| @@ -396,6 +396,9 @@ x-shared-env: &shared-api-worker-env | |||
| 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_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_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576} | |||
| HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True} | |||
| @@ -4,7 +4,7 @@ import type { Area } from 'react-easy-crop' | |||
| import React, { useCallback, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import { RiPencilLine } from '@remixicon/react' | |||
| import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react' | |||
| import { updateUserProfile } from '@/service/common' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput' | |||
| @@ -27,6 +27,8 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { | |||
| const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>() | |||
| const [isShowAvatarPicker, setIsShowAvatarPicker] = 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) => { | |||
| setInputImageInfo( | |||
| @@ -48,6 +50,18 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { | |||
| } | |||
| }, [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({ | |||
| limit: 3, | |||
| disabled: false, | |||
| @@ -86,12 +100,21 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { | |||
| <div className="group relative"> | |||
| <Avatar {...props} /> | |||
| <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" | |||
| 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 /> | |||
| </span> | |||
| </span>} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -115,6 +138,26 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => { | |||
| </Button> | |||
| </div> | |||
| </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> | |||
| </> | |||
| ) | |||
| } | |||
| @@ -13,7 +13,7 @@ import Tooltip from '@/app/components/base/tooltip' | |||
| import { AppType } from '@/types/app' | |||
| import { getNewVar, getVars } from '@/utils/var' | |||
| 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 PromptEditor from '@/app/components/base/prompt-editor' | |||
| import ConfigContext from '@/context/debug-configuration' | |||
| @@ -61,6 +61,7 @@ const Prompt: FC<ISimplePromptInput> = ({ | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| const { | |||
| appId, | |||
| modelConfig, | |||
| dataSets, | |||
| setModelConfig, | |||
| @@ -139,21 +140,21 @@ const Prompt: FC<ISimplePromptInput> = ({ | |||
| } | |||
| 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. | |||
| eventEmitter?.emit({ | |||
| type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER, | |||
| payload: res.prompt, | |||
| payload: res.modified, | |||
| } as any) | |||
| 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) | |||
| setPrevPromptConfig(modelConfig.configs) | |||
| if (mode !== AppType.completion) { | |||
| setIntroduction(res.opening_statement) | |||
| setIntroduction(res.opening_statement || '') | |||
| const newFeatures = produce(features, (draft) => { | |||
| draft.opening = { | |||
| ...draft.opening, | |||
| @@ -272,10 +273,13 @@ const Prompt: FC<ISimplePromptInput> = ({ | |||
| {showAutomatic && ( | |||
| <GetAutomaticResModal | |||
| flowId={appId} | |||
| mode={mode as AppType} | |||
| isShow={showAutomatic} | |||
| onClose={showAutomaticFalse} | |||
| onFinished={handleAutomaticRes} | |||
| currentPrompt={promptTemplate} | |||
| isBasicMode | |||
| /> | |||
| )} | |||
| </div> | |||
| @@ -2,7 +2,7 @@ | |||
| import type { FC } from 'react' | |||
| import React, { useCallback, useEffect, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useBoolean } from 'ahooks' | |||
| import { useBoolean, useSessionStorageState } from 'ahooks' | |||
| import { | |||
| RiDatabase2Line, | |||
| RiFileExcel2Line, | |||
| @@ -14,24 +14,18 @@ import { | |||
| RiTranslate, | |||
| RiUser2Line, | |||
| } from '@remixicon/react' | |||
| import cn from 'classnames' | |||
| import s from './style.module.css' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Button from '@/app/components/base/button' | |||
| import Textarea from '@/app/components/base/textarea' | |||
| 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 { 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 Confirm from '@/app/components/base/confirm' | |||
| import { LoveMessage } from '@/app/components/base/icons/src/vender/features' | |||
| // type | |||
| import type { AutomaticRes } from '@/service/debug' | |||
| import type { GenRes } from '@/service/debug' | |||
| import { Generator } from '@/app/components/base/icons/src/vender/other' | |||
| import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' | |||
| @@ -39,13 +33,25 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro | |||
| import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' | |||
| import type { ModelModeType } from '@/types/app' | |||
| 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 = { | |||
| mode: AppType | |||
| isShow: boolean | |||
| onClose: () => void | |||
| onFinished: (res: AutomaticRes) => void | |||
| isInLLMNode?: boolean | |||
| onFinished: (res: GenRes) => void | |||
| flowId?: string | |||
| nodeId?: string | |||
| currentPrompt?: string | |||
| isBasicMode?: boolean | |||
| } | |||
| const TryLabel: FC<{ | |||
| @@ -68,7 +74,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ | |||
| mode, | |||
| isShow, | |||
| onClose, | |||
| isInLLMNode, | |||
| flowId, | |||
| nodeId, | |||
| currentPrompt, | |||
| isBasicMode, | |||
| onFinished, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| @@ -123,13 +132,27 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ | |||
| }, | |||
| ] | |||
| 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) => { | |||
| return () => { | |||
| const template = t(`appDebug.generate.template.${key}.instruction`) | |||
| setInstruction(template) | |||
| setEditorKey(`${flowId}-${Date.now()}`) | |||
| } | |||
| }, [t]) | |||
| const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.prompt, isBasicMode) | |||
| useEffect(() => { | |||
| if (!instruction && instructionTemplate) | |||
| setInstruction(instructionTemplate.data) | |||
| setEditorKey(`${flowId}-${Date.now()}`) | |||
| }, [instructionTemplate]) | |||
| const isValid = () => { | |||
| if (instruction.trim() === '') { | |||
| Toast.notify({ | |||
| @@ -143,7 +166,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ | |||
| return true | |||
| } | |||
| 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(() => { | |||
| if (defaultModel) { | |||
| @@ -170,16 +196,6 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ | |||
| </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 newModel = { | |||
| ...model, | |||
| @@ -207,28 +223,59 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ | |||
| return | |||
| setLoadingTrue() | |||
| 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 { | |||
| setLoadingFalse() | |||
| } | |||
| } | |||
| const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false) | |||
| const [isShowConfirmOverwrite, { | |||
| setTrue: showConfirmOverwrite, | |||
| setFalse: hideShowConfirmOverwrite, | |||
| }] = useBoolean(false) | |||
| const isShowAutoPromptResPlaceholder = () => { | |||
| return !isLoading && !res | |||
| return !isLoading && !current | |||
| } | |||
| return ( | |||
| @@ -236,15 +283,14 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ | |||
| isShow={isShow} | |||
| onClose={onClose} | |||
| className='min-w-[1140px] !p-0' | |||
| closable | |||
| > | |||
| <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='mb-8'> | |||
| <div className='mb-5'> | |||
| <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> | |||
| <div className='mb-8'> | |||
| <div> | |||
| <ModelParameterModal | |||
| popupClassName='!w-[520px]' | |||
| portalToFollowElemContentClassName='z-[1000]' | |||
| @@ -258,116 +304,99 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({ | |||
| hideDebugWithMultipleModel | |||
| /> | |||
| </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 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 */} | |||
| <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> | |||
| <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 | |||
| className='flex space-x-1' | |||
| variant='primary' | |||
| onClick={onGenerate} | |||
| 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> | |||
| </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> | |||
| )} | |||
| {isLoading && renderLoading} | |||
| {isShowAutoPromptResPlaceholder() && renderNoData} | |||
| {showConfirmOverwrite && ( | |||
| {isShowAutoPromptResPlaceholder() && <ResPlaceholder />} | |||
| {isShowConfirmOverwrite && ( | |||
| <Confirm | |||
| title={t('appDebug.generate.overwriteTitle')} | |||
| content={t('appDebug.generate.overwriteMessage')} | |||
| isShow={showConfirmOverwrite} | |||
| isShow | |||
| onConfirm={() => { | |||
| setShowConfirmOverwrite(false) | |||
| onFinished(res!) | |||
| hideShowConfirmOverwrite() | |||
| onFinished(current!) | |||
| }} | |||
| onCancel={() => setShowConfirmOverwrite(false)} | |||
| onCancel={hideShowConfirmOverwrite} | |||
| /> | |||
| )} | |||
| </div> | |||
| @@ -0,0 +1,48 @@ | |||
| '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) | |||
| @@ -0,0 +1,58 @@ | |||
| '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) | |||
| @@ -0,0 +1,117 @@ | |||
| '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) | |||
| @@ -0,0 +1,55 @@ | |||
| '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) | |||
| @@ -0,0 +1,31 @@ | |||
| '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) | |||
| @@ -0,0 +1,54 @@ | |||
| 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 | |||
| @@ -0,0 +1,18 @@ | |||
| '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) | |||
| @@ -0,0 +1,97 @@ | |||
| '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) | |||
| @@ -3,5 +3,11 @@ | |||
| -webkit-background-clip: text; | |||
| -webkit-text-fill-color: transparent; | |||
| 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; | |||
| } | |||
| @@ -0,0 +1,4 @@ | |||
| export enum GeneratorType { | |||
| prompt = 'prompt', | |||
| code = 'code', | |||
| } | |||
| @@ -0,0 +1,36 @@ | |||
| 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 | |||
| @@ -0,0 +1,103 @@ | |||
| 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 | |||
| @@ -1,16 +1,13 @@ | |||
| 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 { useTranslation } from 'react-i18next' | |||
| import ConfigPrompt from '../../config-prompt' | |||
| 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 { AppType, CompletionParams, Model } from '@/types/app' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Textarea from '@/app/components/base/textarea' | |||
| import Button from '@/app/components/base/button' | |||
| import { Generator } from '@/app/components/base/icons/src/vender/other' | |||
| import Toast from '@/app/components/base/toast' | |||
| @@ -21,17 +18,33 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com | |||
| 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 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 = { | |||
| flowId: string | |||
| nodeId: string | |||
| currentCode?: string | |||
| mode: AppType | |||
| isShow: boolean | |||
| codeLanguages: CodeLanguage | |||
| onClose: () => void | |||
| onFinished: (res: CodeGenRes) => void | |||
| onFinished: (res: GenRes) => void | |||
| } | |||
| export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( | |||
| { | |||
| flowId, | |||
| nodeId, | |||
| currentCode, | |||
| mode, | |||
| isShow, | |||
| codeLanguages, | |||
| @@ -61,9 +74,25 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( | |||
| const { | |||
| defaultModel, | |||
| } = 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 [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 = () => { | |||
| if (instruction.trim() === '') { | |||
| Toast.notify({ | |||
| @@ -97,7 +126,6 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( | |||
| localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) | |||
| }, [model, setModel]) | |||
| const isInLLMNode = true | |||
| const onGenerate = async () => { | |||
| if (!isValid()) | |||
| return | |||
| @@ -105,25 +133,37 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( | |||
| return | |||
| setLoadingTrue() | |||
| try { | |||
| const { error, ...res } = await generateRuleCode({ | |||
| const { error, ...res } = await generateRule({ | |||
| flow_id: flowId, | |||
| node_id: nodeId, | |||
| current: currentCode, | |||
| instruction, | |||
| 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) { | |||
| Toast.notify({ | |||
| type: 'error', | |||
| message: error, | |||
| }) | |||
| } | |||
| else { | |||
| addVersion(res) | |||
| } | |||
| } | |||
| finally { | |||
| setLoadingFalse() | |||
| } | |||
| } | |||
| const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false) | |||
| const [isShowConfirmOverwrite, { | |||
| setTrue: showConfirmOverwrite, | |||
| setFalse: hideShowConfirmOverwrite, | |||
| }] = useBoolean(false) | |||
| useEffect(() => { | |||
| if (defaultModel) { | |||
| @@ -155,30 +195,20 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( | |||
| <div className='text-[13px] text-text-tertiary'>{t('appDebug.codegen.loading')}</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 ( | |||
| <Modal | |||
| isShow={isShow} | |||
| onClose={onClose} | |||
| className='min-w-[1140px] !p-0' | |||
| closable | |||
| > | |||
| <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> | |||
| <div className='mb-8'> | |||
| <div className='mb-4'> | |||
| <ModelParameterModal | |||
| popupClassName='!w-[520px]' | |||
| portalToFollowElemContentClassName='z-[1000]' | |||
| @@ -194,84 +224,60 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = ( | |||
| </div> | |||
| <div> | |||
| <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} | |||
| onChange={e => setInstruction(e.target.value)} | |||
| onChange={setInstruction} | |||
| nodeId={nodeId} | |||
| generatorType={GeneratorType.code} | |||
| isShowCurrentBlock={!!currentCode} | |||
| /> | |||
| </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 | |||
| className='flex space-x-1' | |||
| variant='primary' | |||
| onClick={onGenerate} | |||
| 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> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {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> | |||
| {showConfirmOverwrite && ( | |||
| {isShowConfirmOverwrite && ( | |||
| <Confirm | |||
| title={t('appDebug.codegen.overwriteConfirmTitle')} | |||
| content={t('appDebug.codegen.overwriteConfirmMessage')} | |||
| isShow={showConfirmOverwrite} | |||
| isShow | |||
| onConfirm={() => { | |||
| setShowConfirmOverwrite(false) | |||
| onFinished(res!) | |||
| hideShowConfirmOverwrite() | |||
| onFinished(current!) | |||
| }} | |||
| onCancel={() => setShowConfirmOverwrite(false)} | |||
| onCancel={hideShowConfirmOverwrite} | |||
| /> | |||
| )} | |||
| </Modal> | |||
| @@ -1,43 +0,0 @@ | |||
| '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) | |||
| @@ -1,51 +0,0 @@ | |||
| '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) | |||
| @@ -0,0 +1,6 @@ | |||
| <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> | |||
| @@ -0,0 +1,6 @@ | |||
| <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> | |||
| @@ -0,0 +1,53 @@ | |||
| { | |||
| "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" | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| // 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 | |||
| @@ -0,0 +1,55 @@ | |||
| { | |||
| "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" | |||
| } | |||
| @@ -0,0 +1,20 @@ | |||
| // 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 | |||
| @@ -3,6 +3,7 @@ export { default as Bookmark } from './Bookmark' | |||
| export { default as CheckDone01 } from './CheckDone01' | |||
| export { default as Check } from './Check' | |||
| export { default as ChecklistSquare } from './ChecklistSquare' | |||
| export { default as CodeAssistant } from './CodeAssistant' | |||
| export { default as DotsGrid } from './DotsGrid' | |||
| export { default as Edit02 } from './Edit02' | |||
| export { default as Edit04 } from './Edit04' | |||
| @@ -14,6 +15,7 @@ export { default as LinkExternal02 } from './LinkExternal02' | |||
| export { default as LogIn04 } from './LogIn04' | |||
| export { default as LogOut01 } from './LogOut01' | |||
| export { default as LogOut04 } from './LogOut04' | |||
| export { default as MagicEdit } from './MagicEdit' | |||
| export { default as Menu01 } from './Menu01' | |||
| export { default as Pin01 } from './Pin01' | |||
| export { default as Pin02 } from './Pin02' | |||
| @@ -3,6 +3,10 @@ import { SupportUploadFileTypes, type ValueSelector } from '../../workflow/types | |||
| export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}' | |||
| export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}' | |||
| 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 UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets' | |||
| export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role' | |||
| @@ -1,7 +1,7 @@ | |||
| 'use client' | |||
| import type { FC, ReactNode } from 'react' | |||
| import { useEffect } from 'react' | |||
| import type { FC } from 'react' | |||
| import React, { useEffect } from 'react' | |||
| import type { | |||
| EditorState, | |||
| } from 'lexical' | |||
| @@ -39,6 +39,22 @@ import { | |||
| WorkflowVariableBlockNode, | |||
| WorkflowVariableBlockReplacementBlock, | |||
| } 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 VariableValueBlock from './plugins/variable-value-block' | |||
| import { VariableValueBlockNode } from './plugins/variable-value-block/node' | |||
| @@ -48,8 +64,11 @@ import UpdateBlock from './plugins/update-block' | |||
| import { textToEditorState } from './utils' | |||
| import type { | |||
| ContextBlockType, | |||
| CurrentBlockType, | |||
| ErrorMessageBlockType, | |||
| ExternalToolBlockType, | |||
| HistoryBlockType, | |||
| LastRunBlockType, | |||
| QueryBlockType, | |||
| VariableBlockType, | |||
| WorkflowVariableBlockType, | |||
| @@ -66,7 +85,7 @@ export type PromptEditorProps = { | |||
| compact?: boolean | |||
| wrapperClassName?: string | |||
| className?: string | |||
| placeholder?: string | ReactNode | |||
| placeholder?: string | React.ReactNode | |||
| placeholderClassName?: string | |||
| style?: React.CSSProperties | |||
| value?: string | |||
| @@ -80,6 +99,9 @@ export type PromptEditorProps = { | |||
| variableBlock?: VariableBlockType | |||
| externalToolBlock?: ExternalToolBlockType | |||
| workflowVariableBlock?: WorkflowVariableBlockType | |||
| currentBlock?: CurrentBlockType | |||
| errorMessageBlock?: ErrorMessageBlockType | |||
| lastRunBlock?: LastRunBlockType | |||
| isSupportFileVar?: boolean | |||
| } | |||
| @@ -102,6 +124,9 @@ const PromptEditor: FC<PromptEditorProps> = ({ | |||
| variableBlock, | |||
| externalToolBlock, | |||
| workflowVariableBlock, | |||
| currentBlock, | |||
| errorMessageBlock, | |||
| lastRunBlock, | |||
| isSupportFileVar, | |||
| }) => { | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| @@ -119,6 +144,9 @@ const PromptEditor: FC<PromptEditorProps> = ({ | |||
| QueryBlockNode, | |||
| WorkflowVariableBlockNode, | |||
| VariableValueBlockNode, | |||
| CurrentBlockNode, | |||
| ErrorMessageBlockNode, | |||
| LastRunBlockNode, // LastRunBlockNode is used for error message block replacement | |||
| ], | |||
| editorState: textToEditorState(value || ''), | |||
| onError: (error: Error) => { | |||
| @@ -178,6 +206,9 @@ const PromptEditor: FC<PromptEditorProps> = ({ | |||
| variableBlock={variableBlock} | |||
| externalToolBlock={externalToolBlock} | |||
| workflowVariableBlock={workflowVariableBlock} | |||
| currentBlock={currentBlock} | |||
| errorMessageBlock={errorMessageBlock} | |||
| lastRunBlock={lastRunBlock} | |||
| isSupportFileVar={isSupportFileVar} | |||
| /> | |||
| <ComponentPickerBlock | |||
| @@ -188,6 +219,9 @@ const PromptEditor: FC<PromptEditorProps> = ({ | |||
| variableBlock={variableBlock} | |||
| externalToolBlock={externalToolBlock} | |||
| workflowVariableBlock={workflowVariableBlock} | |||
| currentBlock={currentBlock} | |||
| errorMessageBlock={errorMessageBlock} | |||
| lastRunBlock={lastRunBlock} | |||
| isSupportFileVar={isSupportFileVar} | |||
| /> | |||
| { | |||
| @@ -230,6 +264,35 @@ const PromptEditor: FC<PromptEditorProps> = ({ | |||
| </> | |||
| ) | |||
| } | |||
| { | |||
| currentBlock?.show && ( | |||
| <> | |||
| <CurrentBlock {...currentBlock} /> | |||
| <CurrentBlockReplacementBlock {...currentBlock} /> | |||
| </> | |||
| ) | |||
| } | |||
| { | |||
| errorMessageBlock?.show && ( | |||
| <> | |||
| <ErrorMessageBlock {...errorMessageBlock} /> | |||
| <ErrorMessageBlockReplacementBlock {...errorMessageBlock} /> | |||
| </> | |||
| ) | |||
| } | |||
| { | |||
| lastRunBlock?.show && ( | |||
| <> | |||
| <LastRunBlock {...lastRunBlock} /> | |||
| <LastRunReplacementBlock {...lastRunBlock} /> | |||
| </> | |||
| ) | |||
| } | |||
| { | |||
| isSupportFileVar && ( | |||
| <VariableValueBlock /> | |||
| ) | |||
| } | |||
| <OnChangePlugin onChange={handleEditorChange} /> | |||
| <OnBlurBlock onBlur={onBlur} onFocus={onFocus} /> | |||
| <UpdateBlock instanceId={instanceId} /> | |||
| @@ -4,8 +4,11 @@ import { $insertNodes } from 'lexical' | |||
| import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' | |||
| import type { | |||
| ContextBlockType, | |||
| CurrentBlockType, | |||
| ErrorMessageBlockType, | |||
| ExternalToolBlockType, | |||
| HistoryBlockType, | |||
| LastRunBlockType, | |||
| QueryBlockType, | |||
| VariableBlockType, | |||
| WorkflowVariableBlockType, | |||
| @@ -27,6 +30,7 @@ import { BracketsX } from '@/app/components/base/icons/src/vender/line/developme | |||
| import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users' | |||
| import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import { VarType } from '@/app/components/workflow/types' | |||
| export const usePromptOptions = ( | |||
| contextBlock?: ContextBlockType, | |||
| @@ -267,17 +271,61 @@ export const useOptions = ( | |||
| variableBlock?: VariableBlockType, | |||
| externalToolBlockType?: ExternalToolBlockType, | |||
| workflowVariableBlockType?: WorkflowVariableBlockType, | |||
| currentBlockType?: CurrentBlockType, | |||
| errorMessageBlockType?: ErrorMessageBlockType, | |||
| lastRunBlockType?: LastRunBlockType, | |||
| queryString?: string, | |||
| ) => { | |||
| const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock) | |||
| const variableOptions = useVariableOptions(variableBlock, queryString) | |||
| const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString) | |||
| const workflowVariableOptions = useMemo(() => { | |||
| if (!workflowVariableBlockType?.show) | |||
| 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 { | |||
| @@ -17,8 +17,11 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext | |||
| import { LexicalTypeaheadMenuPlugin } from '@lexical/react/LexicalTypeaheadMenuPlugin' | |||
| import type { | |||
| ContextBlockType, | |||
| CurrentBlockType, | |||
| ErrorMessageBlockType, | |||
| ExternalToolBlockType, | |||
| HistoryBlockType, | |||
| LastRunBlockType, | |||
| QueryBlockType, | |||
| VariableBlockType, | |||
| WorkflowVariableBlockType, | |||
| @@ -32,6 +35,10 @@ import type { PickerBlockMenuOption } from './menu' | |||
| import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| 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 = { | |||
| triggerString: string | |||
| @@ -41,6 +48,9 @@ type ComponentPickerProps = { | |||
| variableBlock?: VariableBlockType | |||
| externalToolBlock?: ExternalToolBlockType | |||
| workflowVariableBlock?: WorkflowVariableBlockType | |||
| currentBlock?: CurrentBlockType | |||
| errorMessageBlock?: ErrorMessageBlockType | |||
| lastRunBlock?: LastRunBlockType | |||
| isSupportFileVar?: boolean | |||
| } | |||
| const ComponentPicker = ({ | |||
| @@ -51,6 +61,9 @@ const ComponentPicker = ({ | |||
| variableBlock, | |||
| externalToolBlock, | |||
| workflowVariableBlock, | |||
| currentBlock, | |||
| errorMessageBlock, | |||
| lastRunBlock, | |||
| isSupportFileVar, | |||
| }: ComponentPickerProps) => { | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| @@ -87,6 +100,9 @@ const ComponentPicker = ({ | |||
| variableBlock, | |||
| externalToolBlock, | |||
| workflowVariableBlock, | |||
| currentBlock, | |||
| errorMessageBlock, | |||
| lastRunBlock, | |||
| ) | |||
| const onSelectOption = useCallback( | |||
| @@ -112,12 +128,23 @@ const ComponentPicker = ({ | |||
| if (needRemove) | |||
| 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]]) | |||
| else | |||
| } | |||
| else { | |||
| editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables) | |||
| }, [editor, checkForTriggerMatch, triggerString]) | |||
| } | |||
| }, [editor, currentBlock?.generatorType, checkForTriggerMatch, triggerString]) | |||
| const handleClose = useCallback(() => { | |||
| const escapeEvent = new KeyboardEvent('keydown', { key: 'Escape' }) | |||
| @@ -168,6 +195,7 @@ const ComponentPicker = ({ | |||
| showManageInputField={workflowVariableBlock.showManageInputField} | |||
| onManageInputField={workflowVariableBlock.onManageInputField} | |||
| autoFocus={false} | |||
| isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code} | |||
| /> | |||
| </div> | |||
| ) | |||
| @@ -208,7 +236,7 @@ const ComponentPicker = ({ | |||
| } | |||
| </> | |||
| ) | |||
| }, [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 ( | |||
| <LexicalTypeaheadMenuPlugin | |||
| @@ -0,0 +1,44 @@ | |||
| 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 | |||
| @@ -0,0 +1,61 @@ | |||
| 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) | |||
| @@ -0,0 +1,66 @@ | |||
| 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' | |||
| @@ -0,0 +1,78 @@ | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| 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 | |||
| @@ -0,0 +1,60 @@ | |||
| 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) | |||
| @@ -0,0 +1,65 @@ | |||
| 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' | |||
| @@ -0,0 +1,67 @@ | |||
| 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 | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| 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 | |||
| @@ -0,0 +1,65 @@ | |||
| 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' | |||
| @@ -0,0 +1,60 @@ | |||
| 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) | |||
| @@ -0,0 +1,67 @@ | |||
| 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 | |||
| } | |||
| @@ -1,3 +1,4 @@ | |||
| import type { GeneratorType } from '../../app/configuration/config/automatic/types' | |||
| import type { Type } from '../../workflow/nodes/llm/types' | |||
| import type { Dataset } from './plugins/context-block' | |||
| import type { RoleName } from './plugins/history-block' | |||
| @@ -77,3 +78,22 @@ export type MenuTextMatch = { | |||
| matchingString: 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 | |||
| } | |||
| @@ -5,7 +5,7 @@ const Tag = ({ text, className }: { text: string; className?: string }) => { | |||
| return ( | |||
| <div className={cn('inline-flex items-center gap-x-0.5', className)}> | |||
| <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> | |||
| ) | |||
| } | |||
| @@ -33,8 +33,383 @@ import { useDocumentArchive, useDocumentDelete, useDocumentDisable, useDocumentE | |||
| import { extensionToFileType } from '@/app/components/datasets/hit-testing/utils/extension-to-file-type' | |||
| import useBatchEditDocumentMetadata from '../metadata/hooks/use-batch-edit-document-metadata' | |||
| 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) => { | |||
| return ( | |||
| @@ -137,7 +512,7 @@ const DocumentList: FC<IDocumentListProps> = ({ | |||
| const result = aValue.localeCompare(bValue) | |||
| return sortOrder === 'asc' ? result : -result | |||
| } | |||
| else { | |||
| else { | |||
| const result = aValue - bValue | |||
| return sortOrder === 'asc' ? result : -result | |||
| } | |||
| @@ -150,7 +525,7 @@ const DocumentList: FC<IDocumentListProps> = ({ | |||
| if (sortField === field) { | |||
| setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc') | |||
| } | |||
| else { | |||
| else { | |||
| setSortField(field) | |||
| setSortOrder('desc') | |||
| } | |||
| @@ -36,7 +36,7 @@ const Category: FC<ICategoryProps> = ({ | |||
| ) | |||
| 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 | |||
| className={itemClassName(isAllCategories)} | |||
| onClick={() => onChange(allCategoriesEn)} | |||
| @@ -2,11 +2,11 @@ export type CommandHandler = (args?: Record<string, any>) => void | Promise<void | |||
| const handlers = new Map<string, CommandHandler>() | |||
| export const registerCommand = (name: string, handler: CommandHandler) => { | |||
| const registerCommand = (name: string, handler: CommandHandler) => { | |||
| handlers.set(name, handler) | |||
| } | |||
| export const unregisterCommand = (name: string) => { | |||
| const unregisterCommand = (name: string) => { | |||
| handlers.delete(name) | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| // 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' | |||
| @@ -0,0 +1,53 @@ | |||
| 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']) | |||
| }, | |||
| } | |||
| @@ -0,0 +1,233 @@ | |||
| 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() | |||
| @@ -0,0 +1,52 @@ | |||
| '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 | |||
| } | |||
| @@ -1,7 +1,15 @@ | |||
| import type { CommandSearchResult } from './types' | |||
| import type { SlashCommandHandler } from './types' | |||
| import type { CommandSearchResult } from '../types' | |||
| 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 { 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 }[] = [ | |||
| { | |||
| @@ -24,7 +32,7 @@ const THEME_ITEMS: { id: 'light' | 'dark' | 'system'; titleKey: string; descKey: | |||
| }, | |||
| ] | |||
| export const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => { | |||
| const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => { | |||
| const q = query.toLowerCase() | |||
| const list = THEME_ITEMS.filter(item => | |||
| !q | |||
| @@ -45,17 +53,28 @@ export const buildThemeCommands = (query: string, locale?: string): CommandSearc | |||
| })) | |||
| } | |||
| 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']) | |||
| }, | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| 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 | |||
| } | |||
| @@ -1,15 +1,180 @@ | |||
| /** | |||
| * 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 { knowledgeAction } from './knowledge' | |||
| import { pluginAction } from './plugin' | |||
| import { workflowNodesAction } from './workflow-nodes' | |||
| import type { ActionItem, SearchResult } from './types' | |||
| import { commandAction } from './run' | |||
| import { slashAction } from './commands' | |||
| export const Actions = { | |||
| slash: slashAction, | |||
| app: appAction, | |||
| knowledge: knowledgeAction, | |||
| plugin: pluginAction, | |||
| run: commandAction, | |||
| node: workflowNodesAction, | |||
| } | |||
| @@ -29,11 +194,13 @@ export const searchAnything = async ( | |||
| } | |||
| } | |||
| if (query.startsWith('@')) | |||
| if (query.startsWith('@') || query.startsWith('/')) | |||
| return [] | |||
| const globalSearchActions = Object.values(Actions) | |||
| // Use Promise.allSettled to handle partial failures gracefully | |||
| const searchPromises = Object.values(Actions).map(async (action) => { | |||
| const searchPromises = globalSearchActions.map(async (action) => { | |||
| try { | |||
| const results = await action.search(query, query, locale) | |||
| return { success: true, data: results, actionType: action.key } | |||
| @@ -54,7 +221,7 @@ export const searchAnything = async ( | |||
| allResults.push(...result.value.data) | |||
| } | |||
| else { | |||
| const actionKey = Object.values(Actions)[index]?.key || 'unknown' | |||
| const actionKey = globalSearchActions[index]?.key || 'unknown' | |||
| failedActions.push(actionKey) | |||
| } | |||
| }) | |||
| @@ -67,10 +234,15 @@ export const searchAnything = async ( | |||
| export const matchAction = (query: string, actions: Record<string, ActionItem>) => { | |||
| 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|$)`) | |||
| return reg.test(query) | |||
| }) | |||
| } | |||
| export * from './types' | |||
| export * from './commands' | |||
| export { appAction, knowledgeAction, pluginAction, workflowNodesAction } | |||
| @@ -1,33 +0,0 @@ | |||
| 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 ' } }, | |||
| } | |||
| } | |||
| @@ -1,97 +0,0 @@ | |||
| '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 | |||
| } | |||
| @@ -44,7 +44,7 @@ export type CommandSearchResult = { | |||
| export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult | CommandSearchResult | |||
| export type ActionItem = { | |||
| key: '@app' | '@knowledge' | '@plugin' | '@node' | '@run' | |||
| key: '@app' | '@knowledge' | '@plugin' | '@node' | '/' | |||
| shortcut: string | |||
| title: string | TypeWithI18N | |||
| description: string | |||
| @@ -69,10 +69,10 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co | |||
| <span className="ml-3 text-sm text-text-secondary"> | |||
| {(() => { | |||
| const keyMap: Record<string, string> = { | |||
| '/': 'app.gotoAnything.actions.slashDesc', | |||
| '@app': 'app.gotoAnything.actions.searchApplicationsDesc', | |||
| '@plugin': 'app.gotoAnything.actions.searchPluginsDesc', | |||
| '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', | |||
| '@run': 'app.gotoAnything.actions.runDesc', | |||
| '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', | |||
| } | |||
| return t(keyMap[action.key]) | |||
| @@ -18,7 +18,7 @@ import InstallFromMarketplace from '../plugins/install-plugin/install-from-marke | |||
| import type { Plugin } from '../plugins/types' | |||
| import { Command } from 'cmdk' | |||
| import CommandSelector from './command-selector' | |||
| import { RunCommandProvider } from './actions/run' | |||
| import { SlashCommandProvider } from './actions/commands' | |||
| type Props = { | |||
| onHide?: () => void | |||
| @@ -34,12 +34,7 @@ const GotoAnything: FC<Props> = ({ | |||
| const [searchQuery, setSearchQuery] = useState<string>('') | |||
| const [cmdVal, setCmdVal] = useState<string>('_') | |||
| const inputRef = useRef<HTMLInputElement>(null) | |||
| const handleNavSearch = useCallback((q: string) => { | |||
| setShow(true) | |||
| setSearchQuery(q) | |||
| setCmdVal('') | |||
| requestAnimationFrame(() => inputRef.current?.focus()) | |||
| }, []) | |||
| // Filter actions based on context | |||
| const Actions = useMemo(() => { | |||
| // Create a filtered copy of actions based on current page context | |||
| @@ -48,9 +43,8 @@ const GotoAnything: FC<Props> = ({ | |||
| return AllActions | |||
| } | |||
| 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]) | |||
| @@ -88,14 +82,18 @@ const GotoAnything: FC<Props> = ({ | |||
| 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(() => { | |||
| if (isCommandsMode) return 'commands' | |||
| const query = searchQueryDebouncedValue.toLowerCase() | |||
| const action = matchAction(query, Actions) | |||
| return action ? action.key : 'general' | |||
| return action | |||
| ? (action.key === '/' ? '@command' : action.key) | |||
| : 'general' | |||
| }, [searchQueryDebouncedValue, Actions, isCommandsMode]) | |||
| const { data: searchResults = [], isLoading, isError, error } = useQuery( | |||
| @@ -140,7 +138,8 @@ const GotoAnything: FC<Props> = ({ | |||
| switch (result.type) { | |||
| case 'command': { | |||
| const action = Object.values(Actions).find(a => a.key === '@run') | |||
| // Execute slash commands | |||
| const action = Actions.slash | |||
| action?.action?.(result) | |||
| break | |||
| } | |||
| @@ -208,7 +207,7 @@ const GotoAnything: FC<Props> = ({ | |||
| </div> | |||
| <div className='mt-1 text-xs text-text-quaternary'> | |||
| {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(', ') }) | |||
| } | |||
| </div> | |||
| @@ -242,6 +241,7 @@ const GotoAnything: FC<Props> = ({ | |||
| return ( | |||
| <> | |||
| <SlashCommandProvider /> | |||
| <Modal | |||
| isShow={show} | |||
| onClose={() => { | |||
| @@ -270,7 +270,7 @@ const GotoAnything: FC<Props> = ({ | |||
| placeholder={t('app.gotoAnything.searchPlaceholder')} | |||
| onChange={(e) => { | |||
| setSearchQuery(e.target.value) | |||
| if (!e.target.value.startsWith('@')) | |||
| if (!e.target.value.startsWith('@') && !e.target.value.startsWith('/')) | |||
| clearSelection() | |||
| }} | |||
| className='flex-1 !border-0 !bg-transparent !shadow-none' | |||
| @@ -330,6 +330,7 @@ const GotoAnything: FC<Props> = ({ | |||
| 'plugin': 'app.gotoAnything.groups.plugins', | |||
| 'knowledge': 'app.gotoAnything.groups.knowledgeBases', | |||
| 'workflow-node': 'app.gotoAnything.groups.workflowNodes', | |||
| 'command': 'app.gotoAnything.groups.commands', | |||
| } | |||
| return t(typeMap[type] || `${type}s`) | |||
| })()} className='p-2 capitalize text-text-secondary'> | |||
| @@ -395,7 +396,6 @@ const GotoAnything: FC<Props> = ({ | |||
| </div> | |||
| </Modal> | |||
| <RunCommandProvider onNavSearch={handleNavSearch} /> | |||
| { | |||
| activePlugin && ( | |||
| <InstallFromMarketplace | |||
| @@ -42,7 +42,7 @@ export default function Support() { | |||
| > | |||
| <MenuItems | |||
| 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 | |||
| `, | |||
| )} | |||
| @@ -45,6 +45,7 @@ import { convertUTCDaySecondsToLocalSeconds, timeOfDayToDayjs } from '../referen | |||
| import useReferenceSetting from '../plugin-page/use-reference-setting' | |||
| import { AUTO_UPDATE_MODE } from '../reference-setting-modal/auto-update-setting/types' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| const i18nPrefix = 'plugin.action' | |||
| @@ -69,6 +70,7 @@ const DetailHeader = ({ | |||
| const { setShowUpdatePluginModal } = useModalContext() | |||
| const { refreshModelProviders } = useProviderContext() | |||
| const invalidateAllToolProviders = useInvalidateAllToolProviders() | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const { | |||
| installation_id, | |||
| @@ -122,6 +124,8 @@ const DetailHeader = ({ | |||
| const { referenceSetting } = useReferenceSetting() | |||
| const { auto_upgrade: autoUpgradeInfo } = referenceSetting || {} | |||
| const isAutoUpgradeEnabled = useMemo(() => { | |||
| if (!enable_marketplace) | |||
| return false | |||
| if (!autoUpgradeInfo || !isFromMarketplace) | |||
| return false | |||
| if(autoUpgradeInfo.strategy_setting === 'disabled') | |||
| @@ -259,7 +259,7 @@ const ReasoningConfigForm: React.FC<Props> = ({ | |||
| className='h-8 grow' | |||
| type='number' | |||
| value={varInput?.value || ''} | |||
| onChange={handleValueChange(variable, type)} | |||
| onChange={e => handleValueChange(variable, type)(e.target.value)} | |||
| placeholder={placeholder?.[language] || placeholder?.en_US} | |||
| /> | |||
| )} | |||
| @@ -10,6 +10,7 @@ import { PermissionType } from '@/app/components/plugins/types' | |||
| import type { AutoUpdateConfig } from './auto-update-setting/types' | |||
| import AutoUpdateSetting from './auto-update-setting' | |||
| import { defaultValue as autoUpdateDefaultValue } from './auto-update-setting/config' | |||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||
| import Label from './label' | |||
| const i18nPrefix = 'plugin.privilege' | |||
| @@ -28,6 +29,7 @@ const PluginSettingModal: FC<Props> = ({ | |||
| const { auto_upgrade: autoUpdateConfig, permission: privilege } = payload || {} | |||
| const [tempPrivilege, setTempPrivilege] = useState<Permissions>(privilege) | |||
| const [tempAutoUpdateConfig, setTempAutoUpdateConfig] = useState<AutoUpdateConfig>(autoUpdateConfig || autoUpdateDefaultValue) | |||
| const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) | |||
| const handlePrivilegeChange = useCallback((key: string) => { | |||
| return (value: PermissionType) => { | |||
| setTempPrivilege({ | |||
| @@ -77,8 +79,11 @@ const PluginSettingModal: FC<Props> = ({ | |||
| </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'> | |||
| <Button | |||
| className='min-w-[72px]' | |||
| @@ -3,7 +3,7 @@ import { | |||
| useCallback, | |||
| useMemo, | |||
| } from 'react' | |||
| import { useStore as useReactflowStore } from 'reactflow' | |||
| import { useEdges, useNodes, useStore as useReactflowStore } from 'reactflow' | |||
| import { RiApps2AddLine } from '@remixicon/react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { | |||
| @@ -11,6 +11,7 @@ import { | |||
| useWorkflowStore, | |||
| } from '@/app/components/workflow/store' | |||
| import { | |||
| useChecklist, | |||
| useChecklistBeforePublish, | |||
| useNodesReadOnly, | |||
| useNodesSyncDraft, | |||
| @@ -18,6 +19,10 @@ import { | |||
| import Button from '@/app/components/base/button' | |||
| import AppPublisher from '@/app/components/app/app-publisher' | |||
| import { useFeatures } from '@/app/components/base/features/hooks' | |||
| import type { | |||
| CommonEdgeType, | |||
| CommonNodeType, | |||
| } from '@/app/components/workflow/types' | |||
| import { | |||
| BlockEnum, | |||
| InputVarType, | |||
| @@ -92,8 +97,19 @@ const FeaturesTrigger = () => { | |||
| } | |||
| }, [appID, setAppDetail]) | |||
| const { mutateAsync: publishWorkflow } = usePublishWorkflow() | |||
| const nodes = useNodes<CommonNodeType>() | |||
| const edges = useEdges<CommonEdgeType>() | |||
| const needWarningNodes = useChecklist(nodes, edges) | |||
| const updatePublishedWorkflow = useInvalidateAppWorkflow() | |||
| 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()) { | |||
| const res = await publishWorkflow({ | |||
| url: `/apps/${appID}/workflows/publish`, | |||
| @@ -112,7 +128,7 @@ const FeaturesTrigger = () => { | |||
| else { | |||
| 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) => { | |||
| if (state) | |||
| @@ -10,6 +10,7 @@ import type { | |||
| } from '@/app/components/workflow/types' | |||
| import { useIsChatMode } from './use-workflow' | |||
| import { useStoreApi } from 'reactflow' | |||
| import type { Type } from '../nodes/llm/types' | |||
| export const useWorkflowVariables = () => { | |||
| const { t } = useTranslation() | |||
| @@ -117,7 +118,7 @@ export const useWorkflowVariableType = () => { | |||
| isChatMode, | |||
| isConstant: false, | |||
| }) | |||
| return type | |||
| return type as unknown as Type | |||
| } | |||
| return getVarType | |||
| @@ -87,6 +87,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { | |||
| headerClassName='bg-transparent px-0 text-text-secondary system-sm-semibold-uppercase' | |||
| containerBackgroundClassName='bg-transparent' | |||
| gradientBorder={false} | |||
| nodeId={nodeId} | |||
| isSupportPromptGenerator={!!def.auto_generate?.type} | |||
| titleTooltip={schema.tooltip && renderI18nObject(schema.tooltip)} | |||
| editorContainerClassName='px-0' | |||
| @@ -7,25 +7,32 @@ import type { CodeLanguage } from '../../code/types' | |||
| import { Generator } from '@/app/components/base/icons/src/vender/other' | |||
| import { ActionButton } from '@/app/components/base/action-button' | |||
| 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 { useHooksStore } from '../../../hooks-store' | |||
| type Props = { | |||
| nodeId: string | |||
| currentCode?: string | |||
| className?: string | |||
| onGenerated?: (prompt: string) => void | |||
| codeLanguages: CodeLanguage | |||
| } | |||
| const CodeGenerateBtn: FC<Props> = ({ | |||
| nodeId, | |||
| currentCode, | |||
| className, | |||
| codeLanguages, | |||
| onGenerated, | |||
| }) => { | |||
| 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() | |||
| }, [onGenerated, showAutomaticFalse]) | |||
| const configsMap = useHooksStore(s => s.configsMap) | |||
| return ( | |||
| <div className={cn(className)}> | |||
| <ActionButton | |||
| @@ -40,6 +47,9 @@ const CodeGenerateBtn: FC<Props> = ({ | |||
| codeLanguages={codeLanguages} | |||
| onClose={showAutomaticFalse} | |||
| onFinished={handleAutomaticRes} | |||
| flowId={configsMap?.flowId || ''} | |||
| nodeId={nodeId} | |||
| currentCode={currentCode} | |||
| /> | |||
| )} | |||
| </div> | |||