Merge remote-tracking branch 'origin/main' into add-endpoint-of-get-feedback

pull/18697/head
lizb 1 year ago
commit 301c6ebcf9

@ -52,7 +52,6 @@ def initialize_extensions(app: DifyApp):
ext_mail,
ext_migrate,
ext_otel,
ext_otel_patch,
ext_proxy_fix,
ext_redis,
ext_repositories,
@ -85,7 +84,6 @@ def initialize_extensions(app: DifyApp):
ext_proxy_fix,
ext_blueprints,
ext_commands,
ext_otel_patch, # Apply patch before initializing OpenTelemetry
ext_otel,
]
for ext in extensions:

@ -16,11 +16,25 @@ AUDIO_EXTENSIONS.extend([ext.upper() for ext in AUDIO_EXTENSIONS])
if dify_config.ETL_TYPE == "Unstructured":
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls"]
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "vtt", "properties"]
DOCUMENT_EXTENSIONS.extend(("doc", "docx", "csv", "eml", "msg", "pptx", "xml", "epub"))
if dify_config.UNSTRUCTURED_API_URL:
DOCUMENT_EXTENSIONS.append("ppt")
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])
else:
DOCUMENT_EXTENSIONS = ["txt", "markdown", "md", "mdx", "pdf", "html", "htm", "xlsx", "xls", "docx", "csv"]
DOCUMENT_EXTENSIONS = [
"txt",
"markdown",
"md",
"mdx",
"pdf",
"html",
"htm",
"xlsx",
"xls",
"docx",
"csv",
"vtt",
"properties",
]
DOCUMENT_EXTENSIONS.extend([ext.upper() for ext in DOCUMENT_EXTENSIONS])

@ -25,8 +25,8 @@ from core.app.entities.task_entities import ChatbotAppBlockingResponse, ChatbotA
from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager
from core.prompt.utils.get_thread_messages_length import get_thread_messages_length
from core.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.repository import RepositoryFactory
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db
from factories import file_factory
from models.account import Account

@ -62,10 +62,10 @@ from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
from core.model_runtime.entities.llm_entities import LLMUsage
from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.ops_trace_manager import TraceQueueManager
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.enums import SystemVariableKey
from core.workflow.graph_engine.entities.graph_runtime_state import GraphRuntimeState
from core.workflow.nodes import NodeType
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from events.message_event import message_was_created
from extensions.ext_database import db
from models import Conversation, EndUser, Message, MessageFile

@ -23,8 +23,8 @@ from core.app.entities.app_invoke_entities import InvokeFrom, WorkflowAppGenerat
from core.app.entities.task_entities import WorkflowAppBlockingResponse, WorkflowAppStreamResponse
from core.model_runtime.errors.invoke import InvokeAuthorizationError
from core.ops.ops_trace_manager import TraceQueueManager
from core.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.repository import RepositoryFactory
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db
from factories import file_factory
from models import Account, App, EndUser, Workflow

@ -54,8 +54,8 @@ from core.app.entities.task_entities import (
from core.app.task_pipeline.based_generate_task_pipeline import BasedGenerateTaskPipeline
from core.app.task_pipeline.workflow_cycle_manage import WorkflowCycleManage
from core.ops.ops_trace_manager import TraceQueueManager
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.enums import SystemVariableKey
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from extensions.ext_database import db
from models.account import Account
from models.enums import CreatedByRole

@ -49,12 +49,12 @@ from core.file import FILE_MODEL_IDENTITY, File
from core.model_runtime.utils.encoders import jsonable_encoder
from core.ops.entities.trace_entity import TraceTaskName
from core.ops.ops_trace_manager import TraceQueueManager, TraceTask
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.tools.tool_manager import ToolManager
from core.workflow.entities.node_entities import NodeRunMetadataKey
from core.workflow.enums import SystemVariableKey
from core.workflow.nodes import NodeType
from core.workflow.nodes.tool.entities import ToolNodeData
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.workflow_entry import WorkflowEntry
from models.account import Account
from models.enums import CreatedByRole, WorkflowRunTriggeredFrom

@ -29,7 +29,7 @@ from core.ops.langfuse_trace.entities.langfuse_trace_entity import (
UnitEnum,
)
from core.ops.utils import filter_none_values
from core.repository.repository_factory import RepositoryFactory
from core.workflow.repository.repository_factory import RepositoryFactory
from extensions.ext_database import db
from models.model import EndUser

@ -28,7 +28,7 @@ from core.ops.langsmith_trace.entities.langsmith_trace_entity import (
LangSmithRunUpdateModel,
)
from core.ops.utils import filter_none_values, generate_dotted_order
from core.repository.repository_factory import RepositoryFactory
from core.workflow.repository.repository_factory import RepositoryFactory
from extensions.ext_database import db
from models.model import EndUser, MessageFile

@ -22,7 +22,7 @@ from core.ops.entities.trace_entity import (
TraceTaskName,
WorkflowTraceInfo,
)
from core.repository.repository_factory import RepositoryFactory
from core.workflow.repository.repository_factory import RepositoryFactory
from extensions.ext_database import db
from models.model import EndUser, MessageFile

@ -72,7 +72,7 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation):
raise ValueError("missing query")
return cls.invoke_chat_app(app, user, conversation_id, query, stream, inputs, files)
elif app.mode == AppMode.WORKFLOW.value:
elif app.mode == AppMode.WORKFLOW:
return cls.invoke_workflow_app(app, user, stream, inputs, files)
elif app.mode == AppMode.COMPLETION:
return cls.invoke_completion_app(app, user, stream, inputs, files)

@ -2,5 +2,5 @@
Repository implementations for data access.
This package contains concrete implementations of the repository interfaces
defined in the core.repository package.
defined in the core.workflow.repository package.
"""

@ -11,9 +11,9 @@ from typing import Any
from sqlalchemy.orm import sessionmaker
from configs import dify_config
from core.repository.repository_factory import RepositoryFactory
from core.repositories.workflow_node_execution import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.repository.repository_factory import RepositoryFactory
from extensions.ext_database import db
from repositories.workflow_node_execution import SQLAlchemyWorkflowNodeExecutionRepository
logger = logging.getLogger(__name__)

@ -2,7 +2,7 @@
WorkflowNodeExecution repository implementations.
"""
from repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
from core.repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
__all__ = [
"SQLAlchemyWorkflowNodeExecutionRepository",

@ -10,7 +10,7 @@ from sqlalchemy import UnaryExpression, asc, delete, desc, select
from sqlalchemy.engine import Engine
from sqlalchemy.orm import sessionmaker
from core.repository.workflow_node_execution_repository import OrderConfig
from core.workflow.repository.workflow_node_execution_repository import OrderConfig
from models.workflow import WorkflowNodeExecution, WorkflowNodeExecutionStatus, WorkflowNodeExecutionTriggeredFrom
logger = logging.getLogger(__name__)

@ -246,7 +246,7 @@ class ToolEngine:
+ "you do not need to create it, just tell the user to check it now."
)
elif response.type == ToolInvokeMessage.MessageType.JSON:
result = json.dumps(
result += json.dumps(
cast(ToolInvokeMessage.JsonMessage, response.message).json_object, ensure_ascii=False
)
else:

@ -11,6 +11,7 @@ import docx
import pandas as pd
import pypandoc # type: ignore
import pypdfium2 # type: ignore
import webvtt # type: ignore
import yaml # type: ignore
from docx.document import Document
from docx.oxml.table import CT_Tbl
@ -132,6 +133,10 @@ def _extract_text_by_mime_type(*, file_content: bytes, mime_type: str) -> str:
return _extract_text_from_json(file_content)
case "application/x-yaml" | "text/yaml":
return _extract_text_from_yaml(file_content)
case "text/vtt":
return _extract_text_from_vtt(file_content)
case "text/properties":
return _extract_text_from_properties(file_content)
case _:
raise UnsupportedFileTypeError(f"Unsupported MIME type: {mime_type}")
@ -139,7 +144,7 @@ def _extract_text_by_mime_type(*, file_content: bytes, mime_type: str) -> str:
def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str) -> str:
"""Extract text from a file based on its file extension."""
match file_extension:
case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml" | ".vtt":
case ".txt" | ".markdown" | ".md" | ".html" | ".htm" | ".xml":
return _extract_text_from_plain_text(file_content)
case ".json":
return _extract_text_from_json(file_content)
@ -165,6 +170,10 @@ def _extract_text_by_file_extension(*, file_content: bytes, file_extension: str)
return _extract_text_from_eml(file_content)
case ".msg":
return _extract_text_from_msg(file_content)
case ".vtt":
return _extract_text_from_vtt(file_content)
case ".properties":
return _extract_text_from_properties(file_content)
case _:
raise UnsupportedFileTypeError(f"Unsupported Extension Type: {file_extension}")
@ -462,3 +471,68 @@ def _extract_text_from_msg(file_content: bytes) -> str:
return "\n".join([str(element) for element in elements])
except Exception as e:
raise TextExtractionError(f"Failed to extract text from MSG: {str(e)}") from e
def _extract_text_from_vtt(vtt_bytes: bytes) -> str:
text = _extract_text_from_plain_text(vtt_bytes)
# remove bom
text = text.lstrip("\ufeff")
raw_results = []
for caption in webvtt.from_string(text):
raw_results.append((caption.voice, caption.text))
# Merge consecutive utterances by the same speaker
merged_results = []
if raw_results:
current_speaker, current_text = raw_results[0]
for i in range(1, len(raw_results)):
spk, txt = raw_results[i]
if spk == None:
merged_results.append((None, current_text))
continue
if spk == current_speaker:
# If it is the same speaker, merge the utterances (joined by space)
current_text += " " + txt
else:
# If the speaker changes, register the utterance so far and move on
merged_results.append((current_speaker, current_text))
current_speaker, current_text = spk, txt
# Add the last element
merged_results.append((current_speaker, current_text))
else:
merged_results = raw_results
# Return the result in the specified format: Speaker "text" style
formatted = [f'{spk or ""} "{txt}"' for spk, txt in merged_results]
return "\n".join(formatted)
def _extract_text_from_properties(file_content: bytes) -> str:
try:
text = _extract_text_from_plain_text(file_content)
lines = text.splitlines()
result = []
for line in lines:
line = line.strip()
# Preserve comments and empty lines
if not line or line.startswith("#") or line.startswith("!"):
result.append(line)
continue
if "=" in line:
key, value = line.split("=", 1)
elif ":" in line:
key, value = line.split(":", 1)
else:
key, value = line, ""
result.append(f"{key.strip()}: {value.strip()}")
return "\n".join(result)
except Exception as e:
raise TextExtractionError(f"Failed to extract text from properties file: {str(e)}") from e

@ -6,8 +6,8 @@ for accessing and manipulating data, regardless of the underlying
storage mechanism.
"""
from core.repository.repository_factory import RepositoryFactory
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.repository.repository_factory import RepositoryFactory
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
__all__ = [
"RepositoryFactory",

@ -8,7 +8,7 @@ It does not contain any implementation details or dependencies on specific repos
from collections.abc import Callable, Mapping
from typing import Any, Literal, Optional, cast
from core.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
from core.workflow.repository.workflow_node_execution_repository import WorkflowNodeExecutionRepository
# Type for factory functions - takes a dict of parameters and returns any repository type
RepositoryFactoryFunc = Callable[[Mapping[str, Any]], Any]

@ -8,192 +8,197 @@ from typing import Union
from celery.signals import worker_init # type: ignore
from flask_login import user_loaded_from_request, user_logged_in # type: ignore
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.metrics import get_meter, get_meter_provider, set_meter_provider
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3Format
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
ConsoleSpanExporter,
)
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import Span, get_current_span, get_tracer_provider, set_tracer_provider
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.trace.status import StatusCode
from configs import dify_config
from dify_app import DifyApp
class ExceptionLoggingHandler(logging.Handler):
"""Custom logging handler that creates spans for logging.exception() calls"""
def emit(self, record):
try:
if record.exc_info:
tracer = get_tracer_provider().get_tracer("dify.exception.logging")
with tracer.start_as_current_span(
"log.exception",
attributes={
"log.level": record.levelname,
"log.message": record.getMessage(),
"log.logger": record.name,
"log.file.path": record.pathname,
"log.file.line": record.lineno,
},
) as span:
span.set_status(StatusCode.ERROR)
span.record_exception(record.exc_info[1])
span.set_attribute("exception.type", record.exc_info[0].__name__)
span.set_attribute("exception.message", str(record.exc_info[1]))
except Exception:
pass
@user_logged_in.connect
@user_loaded_from_request.connect
def on_user_loaded(_sender, user):
if user:
current_span = get_current_span()
if current_span:
current_span.set_attribute("service.tenant.id", user.current_tenant_id)
current_span.set_attribute("service.user.id", user.id)
def init_app(app: DifyApp):
if dify_config.ENABLE_OTEL:
setup_context_propagation()
# Initialize OpenTelemetry
# Follow Semantic Convertions 1.32.0 to define resource attributes
resource = Resource(
attributes={
ResourceAttributes.SERVICE_NAME: dify_config.APPLICATION_NAME,
ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.CURRENT_VERSION}-{dify_config.COMMIT_SHA}",
ResourceAttributes.PROCESS_PID: os.getpid(),
ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}",
ResourceAttributes.HOST_NAME: socket.gethostname(),
ResourceAttributes.HOST_ARCH: platform.machine(),
"custom.deployment.git_commit": dify_config.COMMIT_SHA,
ResourceAttributes.HOST_ID: platform.node(),
ResourceAttributes.OS_TYPE: platform.system().lower(),
ResourceAttributes.OS_DESCRIPTION: platform.platform(),
ResourceAttributes.OS_VERSION: platform.version(),
}
)
sampler = ParentBasedTraceIdRatio(dify_config.OTEL_SAMPLING_RATE)
provider = TracerProvider(resource=resource, sampler=sampler)
set_tracer_provider(provider)
exporter: Union[OTLPSpanExporter, ConsoleSpanExporter]
metric_exporter: Union[OTLPMetricExporter, ConsoleMetricExporter]
if dify_config.OTEL_EXPORTER_TYPE == "otlp":
exporter = OTLPSpanExporter(
endpoint=dify_config.OTLP_BASE_ENDPOINT + "/v1/traces",
headers={"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"},
)
metric_exporter = OTLPMetricExporter(
endpoint=dify_config.OTLP_BASE_ENDPOINT + "/v1/metrics",
headers={"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"},
)
else:
# Fallback to console exporter
exporter = ConsoleSpanExporter()
metric_exporter = ConsoleMetricExporter()
provider.add_span_processor(
BatchSpanProcessor(
exporter,
max_queue_size=dify_config.OTEL_MAX_QUEUE_SIZE,
schedule_delay_millis=dify_config.OTEL_BATCH_EXPORT_SCHEDULE_DELAY,
max_export_batch_size=dify_config.OTEL_MAX_EXPORT_BATCH_SIZE,
export_timeout_millis=dify_config.OTEL_BATCH_EXPORT_TIMEOUT,
)
)
reader = PeriodicExportingMetricReader(
metric_exporter,
export_interval_millis=dify_config.OTEL_METRIC_EXPORT_INTERVAL,
export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT,
)
set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader]))
if not is_celery_worker():
init_flask_instrumentor(app)
CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument()
instrument_exception_logging()
init_sqlalchemy_instrumentor(app)
atexit.register(shutdown_tracer)
from opentelemetry.trace import get_current_span
if user:
current_span = get_current_span()
if current_span:
current_span.set_attribute("service.tenant.id", user.current_tenant_id)
current_span.set_attribute("service.user.id", user.id)
def is_celery_worker():
return "celery" in sys.argv[0].lower()
def init_app(app: DifyApp):
def is_celery_worker():
return "celery" in sys.argv[0].lower()
def instrument_exception_logging():
exception_handler = ExceptionLoggingHandler()
logging.getLogger().addHandler(exception_handler)
def instrument_exception_logging():
exception_handler = ExceptionLoggingHandler()
logging.getLogger().addHandler(exception_handler)
def init_flask_instrumentor(app: DifyApp):
meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION)
_http_response_counter = meter.create_counter(
"http.server.response.count", description="Total number of HTTP responses by status code", unit="{response}"
)
def response_hook(span: Span, status: str, response_headers: list):
if span and span.is_recording():
if status.startswith("2"):
span.set_status(StatusCode.OK)
else:
span.set_status(StatusCode.ERROR, status)
status = status.split(" ")[0]
status_code = int(status)
status_class = f"{status_code // 100}xx"
_http_response_counter.add(1, {"status_code": status_code, "status_class": status_class})
instrumentor = FlaskInstrumentor()
if dify_config.DEBUG:
logging.info("Initializing Flask instrumentor")
instrumentor.instrument_app(app, response_hook=response_hook)
def init_sqlalchemy_instrumentor(app: DifyApp):
with app.app_context():
engines = list(app.extensions["sqlalchemy"].engines.values())
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
def setup_context_propagation():
# Configure propagators
set_global_textmap(
CompositePropagator(
[
TraceContextTextMapPropagator(), # W3C trace context
B3Format(), # B3 propagation (used by many systems)
]
)
)
def init_flask_instrumentor(app: DifyApp):
meter = get_meter("http_metrics", version=dify_config.CURRENT_VERSION)
_http_response_counter = meter.create_counter(
"http.server.response.count", description="Total number of HTTP responses by status code", unit="{response}"
def shutdown_tracer():
provider = trace.get_tracer_provider()
if hasattr(provider, "force_flush"):
provider.force_flush()
class ExceptionLoggingHandler(logging.Handler):
"""Custom logging handler that creates spans for logging.exception() calls"""
def emit(self, record):
try:
if record.exc_info:
tracer = get_tracer_provider().get_tracer("dify.exception.logging")
with tracer.start_as_current_span(
"log.exception",
attributes={
"log.level": record.levelname,
"log.message": record.getMessage(),
"log.logger": record.name,
"log.file.path": record.pathname,
"log.file.line": record.lineno,
},
) as span:
span.set_status(StatusCode.ERROR)
span.record_exception(record.exc_info[1])
span.set_attribute("exception.type", record.exc_info[0].__name__)
span.set_attribute("exception.message", str(record.exc_info[1]))
except Exception:
pass
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.metrics import get_meter, get_meter_provider, set_meter_provider
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.b3 import B3Format
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
BatchSpanProcessor,
ConsoleSpanExporter,
)
def response_hook(span: Span, status: str, response_headers: list):
if span and span.is_recording():
if status.startswith("2"):
span.set_status(StatusCode.OK)
else:
span.set_status(StatusCode.ERROR, status)
status = status.split(" ")[0]
status_code = int(status)
status_class = f"{status_code // 100}xx"
_http_response_counter.add(1, {"status_code": status_code, "status_class": status_class})
instrumentor = FlaskInstrumentor()
if dify_config.DEBUG:
logging.info("Initializing Flask instrumentor")
instrumentor.instrument_app(app, response_hook=response_hook)
def init_sqlalchemy_instrumentor(app: DifyApp):
with app.app_context():
engines = list(app.extensions["sqlalchemy"].engines.values())
SQLAlchemyInstrumentor().instrument(enable_commenter=True, engines=engines)
def setup_context_propagation():
# Configure propagators
set_global_textmap(
CompositePropagator(
[
TraceContextTextMapPropagator(), # W3C trace context
B3Format(), # B3 propagation (used by many systems)
]
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.trace import Span, get_tracer_provider, set_tracer_provider
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.trace.status import StatusCode
setup_context_propagation()
# Initialize OpenTelemetry
# Follow Semantic Convertions 1.32.0 to define resource attributes
resource = Resource(
attributes={
ResourceAttributes.SERVICE_NAME: dify_config.APPLICATION_NAME,
ResourceAttributes.SERVICE_VERSION: f"dify-{dify_config.CURRENT_VERSION}-{dify_config.COMMIT_SHA}",
ResourceAttributes.PROCESS_PID: os.getpid(),
ResourceAttributes.DEPLOYMENT_ENVIRONMENT: f"{dify_config.DEPLOY_ENV}-{dify_config.EDITION}",
ResourceAttributes.HOST_NAME: socket.gethostname(),
ResourceAttributes.HOST_ARCH: platform.machine(),
"custom.deployment.git_commit": dify_config.COMMIT_SHA,
ResourceAttributes.HOST_ID: platform.node(),
ResourceAttributes.OS_TYPE: platform.system().lower(),
ResourceAttributes.OS_DESCRIPTION: platform.platform(),
ResourceAttributes.OS_VERSION: platform.version(),
}
)
sampler = ParentBasedTraceIdRatio(dify_config.OTEL_SAMPLING_RATE)
provider = TracerProvider(resource=resource, sampler=sampler)
set_tracer_provider(provider)
exporter: Union[OTLPSpanExporter, ConsoleSpanExporter]
metric_exporter: Union[OTLPMetricExporter, ConsoleMetricExporter]
if dify_config.OTEL_EXPORTER_TYPE == "otlp":
exporter = OTLPSpanExporter(
endpoint=dify_config.OTLP_BASE_ENDPOINT + "/v1/traces",
headers={"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"},
)
metric_exporter = OTLPMetricExporter(
endpoint=dify_config.OTLP_BASE_ENDPOINT + "/v1/metrics",
headers={"Authorization": f"Bearer {dify_config.OTLP_API_KEY}"},
)
else:
# Fallback to console exporter
exporter = ConsoleSpanExporter()
metric_exporter = ConsoleMetricExporter()
provider.add_span_processor(
BatchSpanProcessor(
exporter,
max_queue_size=dify_config.OTEL_MAX_QUEUE_SIZE,
schedule_delay_millis=dify_config.OTEL_BATCH_EXPORT_SCHEDULE_DELAY,
max_export_batch_size=dify_config.OTEL_MAX_EXPORT_BATCH_SIZE,
export_timeout_millis=dify_config.OTEL_BATCH_EXPORT_TIMEOUT,
)
)
reader = PeriodicExportingMetricReader(
metric_exporter,
export_interval_millis=dify_config.OTEL_METRIC_EXPORT_INTERVAL,
export_timeout_millis=dify_config.OTEL_METRIC_EXPORT_TIMEOUT,
)
set_meter_provider(MeterProvider(resource=resource, metric_readers=[reader]))
if not is_celery_worker():
init_flask_instrumentor(app)
CeleryInstrumentor(tracer_provider=get_tracer_provider(), meter_provider=get_meter_provider()).instrument()
instrument_exception_logging()
init_sqlalchemy_instrumentor(app)
atexit.register(shutdown_tracer)
@worker_init.connect(weak=False)
def init_celery_worker(*args, **kwargs):
tracer_provider = get_tracer_provider()
metric_provider = get_meter_provider()
if dify_config.DEBUG:
logging.info("Initializing OpenTelemetry for Celery worker")
CeleryInstrumentor(tracer_provider=tracer_provider, meter_provider=metric_provider).instrument()
def is_enabled():
return dify_config.ENABLE_OTEL
def shutdown_tracer():
provider = trace.get_tracer_provider()
if hasattr(provider, "force_flush"):
provider.force_flush()
@worker_init.connect(weak=False)
def init_celery_worker(*args, **kwargs):
if dify_config.ENABLE_OTEL:
from opentelemetry.instrumentation.celery import CeleryInstrumentor
from opentelemetry.metrics import get_meter_provider
from opentelemetry.trace import get_tracer_provider
tracer_provider = get_tracer_provider()
metric_provider = get_meter_provider()
if dify_config.DEBUG:
logging.info("Initializing OpenTelemetry for Celery worker")
CeleryInstrumentor(tracer_provider=tracer_provider, meter_provider=metric_provider).instrument()

@ -1,63 +0,0 @@
"""
Patch for OpenTelemetry context detach method to handle None tokens gracefully.
This patch addresses the issue where OpenTelemetry's context.detach() method raises a TypeError
when called with a None token. The error occurs in the contextvars_context.py file where it tries
to call reset() on a None token.
Related GitHub issue: https://github.com/langgenius/dify/issues/18496
Error being fixed:
```
Traceback (most recent call last):
File "opentelemetry/context/__init__.py", line 154, in detach
_RUNTIME_CONTEXT.detach(token)
File "opentelemetry/context/contextvars_context.py", line 50, in detach
self._current_context.reset(token) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: expected an instance of Token, got None
```
Instead of modifying the third-party package directly, this patch monkey-patches the
context.detach method to gracefully handle None tokens.
"""
import logging
from functools import wraps
from opentelemetry import context
logger = logging.getLogger(__name__)
# Store the original detach method
original_detach = context.detach
# Create a patched version that handles None tokens
@wraps(original_detach)
def patched_detach(token):
"""
A patched version of context.detach that handles None tokens gracefully.
"""
if token is None:
logger.debug("Attempted to detach a None token, skipping")
return
return original_detach(token)
def is_enabled():
"""
Check if the extension is enabled.
Always enable this patch to prevent errors even when OpenTelemetry is disabled.
"""
return True
def init_app(app):
"""
Initialize the OpenTelemetry context patch.
"""
# Replace the original detach method with our patched version
context.detach = patched_detach
logger.info("OpenTelemetry context.detach patched to handle None tokens")

@ -4,8 +4,8 @@ Extension for initializing repositories.
This extension registers repository implementations with the RepositoryFactory.
"""
from core.repositories.repository_registry import register_repositories
from dify_app import DifyApp
from repositories.repository_registry import register_repositories
def init_app(_app: DifyApp) -> None:

@ -1012,7 +1012,9 @@ class Message(db.Model): # type: ignore[name-defined]
sign_url = file_helpers.get_signed_file_url(upload_file_id)
else:
continue
# if as_attachment is in the url, add it to the sign_url.
if "as_attachment" in url:
sign_url += "&as_attachment=true"
re_sign_file_url_answer = re_sign_file_url_answer.replace(url, sign_url)
return re_sign_file_url_answer

@ -84,6 +84,7 @@ dependencies = [
"validators==0.21.0",
"weave~=0.51.34",
"yarl~=1.18.3",
"webvtt-py~=0.5.1",
]
# Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group.

@ -2,8 +2,8 @@ import threading
from typing import Optional
import contexts
from core.repository import RepositoryFactory
from core.repository.workflow_node_execution_repository import OrderConfig
from core.workflow.repository import RepositoryFactory
from core.workflow.repository.workflow_node_execution_repository import OrderConfig
from extensions.ext_database import db
from libs.infinite_scroll_pagination import InfiniteScrollPagination
from models.enums import WorkflowRunTriggeredFrom

@ -11,7 +11,6 @@ from sqlalchemy.orm import Session
from core.app.apps.advanced_chat.app_config_manager import AdvancedChatAppConfigManager
from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager
from core.model_runtime.utils.encoders import jsonable_encoder
from core.repository import RepositoryFactory
from core.variables import Variable
from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.errors import WorkflowNodeRunFailedError
@ -22,6 +21,7 @@ from core.workflow.nodes.enums import ErrorStrategy
from core.workflow.nodes.event import RunCompletedEvent
from core.workflow.nodes.event.types import NodeEvent
from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING
from core.workflow.repository import RepositoryFactory
from core.workflow.workflow_entry import WorkflowEntry
from events.app_event import app_draft_workflow_was_synced, app_published_workflow_was_updated
from extensions.ext_database import db

@ -7,7 +7,7 @@ from celery import shared_task # type: ignore
from sqlalchemy import delete
from sqlalchemy.exc import SQLAlchemyError
from core.repository import RepositoryFactory
from core.workflow.repository import RepositoryFactory
from extensions.ext_database import db
from models.dataset import AppDatasetJoin
from models.model import (

@ -8,9 +8,9 @@ import pytest
from pytest_mock import MockerFixture
from sqlalchemy.orm import Session, sessionmaker
from core.repository.workflow_node_execution_repository import OrderConfig
from core.repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
from core.workflow.repository.workflow_node_execution_repository import OrderConfig
from models.workflow import WorkflowNodeExecution
from repositories.workflow_node_execution.sqlalchemy_repository import SQLAlchemyWorkflowNodeExecutionRepository
@pytest.fixture
@ -80,7 +80,7 @@ def test_get_by_node_execution_id(repository, session, mocker: MockerFixture):
"""Test get_by_node_execution_id method."""
session_obj, _ = session
# Set up mock
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_select = mocker.patch("core.repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_stmt = mocker.MagicMock()
mock_select.return_value = mock_stmt
mock_stmt.where.return_value = mock_stmt
@ -99,7 +99,7 @@ def test_get_by_workflow_run(repository, session, mocker: MockerFixture):
"""Test get_by_workflow_run method."""
session_obj, _ = session
# Set up mock
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_select = mocker.patch("core.repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_stmt = mocker.MagicMock()
mock_select.return_value = mock_stmt
mock_stmt.where.return_value = mock_stmt
@ -120,7 +120,7 @@ def test_get_running_executions(repository, session, mocker: MockerFixture):
"""Test get_running_executions method."""
session_obj, _ = session
# Set up mock
mock_select = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_select = mocker.patch("core.repositories.workflow_node_execution.sqlalchemy_repository.select")
mock_stmt = mocker.MagicMock()
mock_select.return_value = mock_stmt
mock_stmt.where.return_value = mock_stmt
@ -158,7 +158,7 @@ def test_clear(repository, session, mocker: MockerFixture):
"""Test clear method."""
session_obj, _ = session
# Set up mock
mock_delete = mocker.patch("repositories.workflow_node_execution.sqlalchemy_repository.delete")
mock_delete = mocker.patch("core.repositories.workflow_node_execution.sqlalchemy_repository.delete")
mock_stmt = mocker.MagicMock()
mock_delete.return_value = mock_stmt
mock_stmt.where.return_value = mock_stmt

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
import type { FC } from 'react'
import React from 'react'
import { RiResetLeftLine } from '@remixicon/react'
import React, { useCallback, useEffect, useState } from 'react'
import { RiCollapseDiagonal2Line, RiExpandDiagonal2Line, RiResetLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import type { Theme } from '../theme/theme-context'
import { CssTransform } from '../theme/utils'
@ -36,6 +36,44 @@ const Header: FC<IHeaderProps> = ({
currentConversationId,
inputsForms,
} = useEmbeddedChatbotContext()
const isClient = typeof window !== 'undefined'
const isIframe = isClient ? window.self !== window.top : false
const [parentOrigin, setParentOrigin] = useState('')
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)
const [expanded, setExpanded] = useState(false)
const handleMessageReceived = useCallback((event: MessageEvent) => {
let currentParentOrigin = parentOrigin
if (!currentParentOrigin && event.data.type === 'dify-chatbot-config') {
currentParentOrigin = event.origin
setParentOrigin(event.origin)
}
if (event.origin !== currentParentOrigin)
return
if (event.data.type === 'dify-chatbot-config')
setShowToggleExpandButton(event.data.payload.isToggledByButton && !event.data.payload.isDraggable)
}, [parentOrigin])
useEffect(() => {
if (!isIframe) return
const listener = (event: MessageEvent) => handleMessageReceived(event)
window.addEventListener('message', listener)
window.parent.postMessage({ type: 'dify-chatbot-iframe-ready' }, '*')
return () => window.removeEventListener('message', listener)
}, [isIframe, handleMessageReceived])
const handleToggleExpand = useCallback(() => {
if (!isIframe || !showToggleExpandButton) return
setExpanded(!expanded)
window.parent.postMessage({
type: 'dify-chatbot-expand-change',
}, parentOrigin)
}, [isIframe, parentOrigin, showToggleExpandButton, expanded])
if (!isMobile) {
return (
<div className='flex h-14 shrink-0 items-center justify-end p-3'>
@ -59,6 +97,21 @@ const Header: FC<IHeaderProps> = ({
{currentConversationId && (
<Divider type='vertical' className='h-3.5' />
)}
{
showToggleExpandButton && (
<Tooltip
popupContent={expanded ? t('share.chat.collapse') : t('share.chat.expand')}
>
<ActionButton size='l' onClick={handleToggleExpand}>
{
expanded
? <RiCollapseDiagonal2Line className='h-[18px] w-[18px]' />
: <RiExpandDiagonal2Line className='h-[18px] w-[18px]' />
}
</ActionButton>
</Tooltip>
)
}
{currentConversationId && allowResetChat && (
<Tooltip
popupContent={t('share.chat.resetChat')}
@ -91,6 +144,21 @@ const Header: FC<IHeaderProps> = ({
</div>
</div>
<div className='flex items-center gap-1'>
{
showToggleExpandButton && (
<Tooltip
popupContent={expanded ? t('share.chat.collapse') : t('share.chat.expand')}
>
<ActionButton size='l' onClick={handleToggleExpand}>
{
expanded
? <RiCollapseDiagonal2Line className={cn('h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
: <RiExpandDiagonal2Line className={cn('h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
}
</ActionButton>
</Tooltip>
)
}
{currentConversationId && allowResetChat && (
<Tooltip
popupContent={t('share.chat.resetChat')}

@ -196,22 +196,68 @@ const FileUploader = ({
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
}
type FileWithPath = {
relativePath?: string
} & File
const traverseFileEntry = useCallback(
(entry: any, prefix = ''): Promise<FileWithPath[]> => {
return new Promise((resolve) => {
if (entry.isFile) {
entry.file((file: FileWithPath) => {
file.relativePath = `${prefix}${file.name}`
resolve([file])
})
}
else if (entry.isDirectory) {
const reader = entry.createReader()
const entries: any[] = []
const read = () => {
reader.readEntries(async (results: FileSystemEntry[]) => {
if (!results.length) {
const files = await Promise.all(
entries.map(ent =>
traverseFileEntry(ent, `${prefix}${entry.name}/`),
),
)
resolve(files.flat())
}
else {
entries.push(...results)
read()
}
})
}
read()
}
else {
resolve([])
}
})
},
[],
)
const handleDrop = useCallback((e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragging(false)
if (!e.dataTransfer)
return
let files = [...e.dataTransfer.files] as File[]
if (notSupportBatchUpload)
files = files.slice(0, 1)
const validFiles = files.filter(isValid)
initialUpload(validFiles)
}, [initialUpload, isValid, notSupportBatchUpload])
const handleDrop = useCallback(
async (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
setDragging(false)
if (!e.dataTransfer) return
const nested = await Promise.all(
Array.from(e.dataTransfer.items).map((it) => {
const entry = (it as any).webkitGetAsEntry?.()
if (entry) return traverseFileEntry(entry)
const f = it.getAsFile?.()
return f ? Promise.resolve([f]) : Promise.resolve([])
}),
)
let files = nested.flat()
if (notSupportBatchUpload) files = files.slice(0, 1)
const valid = files.filter(isValid)
initialUpload(valid)
},
[initialUpload, isValid, notSupportBatchUpload, traverseFileEntry],
)
const selectHandle = () => {
if (fileUploader.current)
fileUploader.current.click()

@ -49,7 +49,7 @@ const Header = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSegment])
return (
<div className='flex flex-1 items-center justify-between bg-background-body px-4'>
<div className='relative flex flex-1 items-center justify-between bg-background-body'>
<div className='flex items-center'>
{isMobile && <div
className='flex h-8 w-8 cursor-pointer items-center justify-center'
@ -59,7 +59,7 @@ const Header = () => {
</div>}
{
!isMobile
&& <div className='flex w-64 shrink-0 items-center gap-1.5 self-stretch p-2 pl-3'>
&& <div className='flex shrink-0 items-center gap-1.5 self-stretch pl-3'>
<Link href="/apps" className='flex h-8 w-8 shrink-0 items-center justify-center gap-2'>
<LogoSite className='object-contain' />
</Link>
@ -84,7 +84,7 @@ const Header = () => {
)}
{
!isMobile && (
<div className='flex items-center'>
<div className='absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center'>
{!isCurrentWorkspaceDatasetOperator && <ExploreNav className={navClassName} />}
{!isCurrentWorkspaceDatasetOperator && <AppNav />}
{(isCurrentWorkspaceEditor || isCurrentWorkspaceDatasetOperator) && <DatasetNav />}
@ -92,7 +92,7 @@ const Header = () => {
</div>
)
}
<div className='flex shrink-0 items-center'>
<div className='flex shrink-0 items-center pr-3'>
<EnvNav />
<div className='mr-2'>
<PluginsNav />

@ -596,17 +596,16 @@ const getIterationItemType = ({
arrayType = curr.find((v: any) => v.variable === (valueSelector).join('.'))?.type
}
else {
(valueSelector).slice(1).forEach((key, i) => {
for (let i = 1; i < valueSelector.length - 1; i++) {
const key = valueSelector[i]
const isLast = i === valueSelector.length - 2
curr = curr?.find((v: any) => v.variable === key)
if (isLast) {
arrayType = curr?.type
}
else {
if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children
}
})
curr = Array.isArray(curr) ? curr.find(v => v.variable === key) : []
if (isLast)
arrayType = curr?.type
else if (curr?.type === VarType.object || curr?.type === VarType.file)
curr = curr.children || []
}
}
switch (arrayType as VarType) {
@ -631,7 +630,7 @@ const getLoopItemType = ({
}: {
valueSelector: ValueSelector
beforeNodesOutputVars: NodeOutPutVar[]
// eslint-disable-next-line sonarjs/no-identical-functions
}): VarType => {
const outputVarNodeId = valueSelector[0]
const isSystem = isSystemVar(valueSelector)

@ -296,7 +296,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
onCollapse={setStructuredOutputCollapsed}
operations={
<div className='mr-4 flex shrink-0 items-center'>
{!isModelSupportStructuredOutput && (
{(!isModelSupportStructuredOutput && !!inputs.structured_output_enabled) && (
<Tooltip noDecoration popupContent={
<div className='w-[232px] rounded-xl border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-4 py-3.5 shadow-lg backdrop-blur-[5px]'>
<div className='title-xs-semi-bold text-text-primary'>{t('app.structOutput.modelNotSupported')}</div>

@ -63,7 +63,7 @@ const ClassList: FC<Props> = ({
return (
<Item
nodeId={nodeId}
key={index}
key={list[index].id}
payload={item}
onChange={handleClassChange(index)}
onRemove={handleRemoveClass(index)}

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: 'Textdatei hochladen',
button: 'Datei hierher ziehen oder',
button: 'Dateien und Ordner hierher ziehen oder klicken',
browse: 'Durchsuchen',
tip: 'Unterstützt {{supportTypes}}. Maximal {{size}}MB pro Datei.',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: 'Versuchen zu lösen',
temporarySystemIssue: 'Entschuldigung, vorübergehendes Systemproblem.',
expand: 'Erweitern',
collapse: 'Reduzieren',
},
generation: {
tabs: {

@ -35,7 +35,7 @@ const translation = {
},
uploader: {
title: 'Upload file',
button: 'Drag and drop file, or',
button: 'Drag and drop file or folder, or',
browse: 'Browse',
tip: 'Supports {{supportTypes}}. Max {{size}}MB each.',
validation: {

@ -34,6 +34,8 @@ const translation = {
},
tryToSolve: 'Try to solve',
temporarySystemIssue: 'Sorry, temporary system issue.',
expand: 'Expand',
collapse: 'Collapse',
},
generation: {
tabs: {

@ -27,7 +27,7 @@ const translation = {
},
uploader: {
title: 'Cargar archivo',
button: 'Arrastra y suelta el archivo, o',
button: 'Arrastre y suelte archivos o carpetas, o',
browse: 'Buscar',
tip: 'Soporta {{supportTypes}}. Máximo {{size}}MB cada uno.',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: 'Intentar resolver',
temporarySystemIssue: 'Lo sentimos, hay un problema temporal del sistema.',
expand: 'Ampliar',
collapse: 'Contraer',
},
generation: {
tabs: {

@ -27,7 +27,7 @@ const translation = {
},
uploader: {
title: 'بارگذاری فایل',
button: 'کشیدن و رها کردن فایل، یا',
button: 'فایل ها یا پوشه ها را بکشید و رها کنید یا',
browse: 'مرور',
tip: 'پشتیبانی از {{supportTypes}}. حداکثر {{size}}MB هر کدام.',
validation: {

@ -26,6 +26,8 @@ const translation = {
},
tryToSolve: 'سعی کنید حل کنید',
temporarySystemIssue: 'ببخشید، مشکل موقت سیستمی.',
expand: 'باز کردن',
collapse: 'بستن',
},
generation: {
tabs: {

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: 'Télécharger le fichier texte',
button: 'Glisser et déposer le fichier, ou',
button: 'Faites glisser et déposez des fichiers ou des dossiers, ou',
browse: 'Parcourir',
tip: 'Prend en charge {{supportTypes}}. Max {{size}}MB chacun.',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: 'Essayez de résoudre',
temporarySystemIssue: 'Désolé, problème temporaire du système.',
expand: 'Développer',
collapse: 'Réduire',
},
generation: {
tabs: {

@ -27,7 +27,7 @@ const translation = {
},
uploader: {
title: 'फ़ाइल अपलोड करें',
button: 'फ़ाइल खींचें और छोड़ें, या',
button: 'फ़ाइलों या फ़ोल्डरों को खींचें और छोड़ें, या',
browse: 'ब्राउज़ करें',
tip: 'समर्थित {{supportTypes}}। प्रत्येक अधिकतम {{size}}MB।',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: 'समाधान करने का प्रयास करें',
temporarySystemIssue: 'अभी सिस्टम में समस्या है, कृपया पुनः प्रयास करें।',
expand: 'विस्तार करें',
collapse: 'संकुचित करें',
},
generation: {
tabs: {

@ -27,7 +27,7 @@ const translation = {
},
uploader: {
title: 'Carica file',
button: 'Trascina e rilascia il file, o',
button: 'Trascina e rilascia file o cartelle, oppure',
browse: 'Sfoglia',
tip: 'Supporta {{supportTypes}}. Max {{size}}MB ciascuno.',
validation: {

@ -28,6 +28,8 @@ const translation = {
},
tryToSolve: 'Prova a risolvere',
temporarySystemIssue: 'Spiacente, problema temporaneo del sistema.',
expand: 'Espandi',
collapse: 'Riduci',
},
generation: {
tabs: {

@ -30,7 +30,7 @@ const translation = {
},
uploader: {
title: 'テキストファイルをアップロード',
button: 'ファイルをドラッグ&ドロップするか',
button: 'ファイルまたはフォルダをドラッグアンドドロップする',
browse: '参照',
tip: '{{supportTypes}}をサポートしています。1つあたりの最大サイズは{{size}}MBです。',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: '問題を解決する',
temporarySystemIssue: 'システムに一時的な問題が発生しています',
expand: '拡大',
collapse: '縮小',
},
generation: {
tabs: {

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: '텍스트 파일 업로드',
button: '파일을 끌어다 놓거나',
button: '파일이나 폴더를 끌어서 놓기',
browse: '찾아보기',
tip: '{{supportTypes}}을(를) 지원합니다. 파일당 최대 크기는 {{size}}MB입니다.',
validation: {

@ -26,6 +26,8 @@ const translation = {
},
tryToSolve: '해결하려고 합니다',
temporarySystemIssue: '죄송합니다. 일시적인 시스템 문제가 발생했습니다.',
expand: '확장',
collapse: '축소',
},
generation: {
tabs: {

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: 'Prześlij plik tekstowy',
button: 'Przeciągnij i upuść plik lub',
button: 'Przeciągnij i upuść pliki lub foldery lub',
browse: 'Przeglądaj',
tip: 'Obsługuje {{supportTypes}}. Maksymalnie {{size}}MB każdy.',
validation: {

@ -27,6 +27,8 @@ const translation = {
},
tryToSolve: 'Spróbuj rozwiązać',
temporarySystemIssue: 'Przepraszamy, tymczasowy problem systemowy.',
expand: 'Rozwiń',
collapse: 'Zwiń',
},
generation: {
tabs: {

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: 'Enviar arquivo de texto',
button: 'Arraste e solte o arquivo, ou',
button: 'Arraste e solte arquivos ou pastas, ou',
browse: 'Navegar',
tip: 'Suporta {{supportTypes}}. Máximo de {{size}}MB cada.',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: 'Tente resolver',
temporarySystemIssue: 'Desculpe, problema temporário do sistema.',
expand: 'Expandir',
collapse: 'Contrair',
},
generation: {
tabs: {

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: 'Încărcați fișier text',
button: 'Trageți și fixați fișierul, sau',
button: 'Trageți și plasați fișiere sau foldere sau',
browse: 'Răsfoire',
tip: 'Acceptă {{supportTypes}}. Maxim {{size}}MB fiecare.',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: 'Încercați să rezolvați',
temporarySystemIssue: 'Ne pare rău, problemă temporară a sistemului.',
expand: 'Extinde',
collapse: 'Restrânge',
},
generation: {
tabs: {

@ -27,7 +27,7 @@ const translation = {
},
uploader: {
title: 'Загрузить файл',
button: 'Перетащите файл или',
button: 'Перетащите файлы или папки или',
browse: 'Обзор',
tip: 'Поддерживаются {{supportTypes}}. Максимум {{size}} МБ каждый.',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: 'Попробуйте решить',
temporarySystemIssue: 'Извините, временная проблема с системой.',
expand: 'Развернуть',
collapse: 'Свернуть',
},
generation: {
tabs: {

@ -32,7 +32,7 @@ const translation = {
},
uploader: {
title: 'Naloži datoteko',
button: 'Povleci in spusti datoteko ali',
button: 'Povleci in spusti datoteke ali mape oz',
browse: 'Prebrskaj',
tip: 'Podprti tipi datotek: {{supportTypes}}. Največ {{size}}MB na datoteko.',
validation: {

@ -27,6 +27,8 @@ const translation = {
},
tryToSolve: 'Poskusite rešiti',
temporarySystemIssue: 'Oprostite, začasna težava s sistemom.',
expand: 'Razširi',
collapse: 'Skrči',
},
generation: {
tabs: {

@ -32,7 +32,7 @@ const translation = {
},
uploader: {
title: 'อัปโหลดไฟล์',
button: 'ลากและวางไฟล์ หรือ',
button: 'ลากและวางไฟล์หรือโฟลเดอร์หรือ',
browse: 'เล็ม',
tip: 'รองรับ {{supportTypes}} สูงสุด {{size}}MB แต่ละตัว',
validation: {

@ -26,6 +26,8 @@ const translation = {
},
tryToSolve: 'พยายามแก้',
temporarySystemIssue: 'ขออภัย ปัญหาระบบชั่วคราว',
expand: 'ขยาย',
collapse: 'ย่อ',
},
generation: {
tabs: {

@ -27,7 +27,7 @@ const translation = {
},
uploader: {
title: 'Dosya yükle',
button: 'Dosyayı sürükleyip bırakın veya',
button: 'Dosyaları veya klasörleri sürükleyip bırakın veya',
browse: 'Göz atın',
tip: 'Destekler {{supportTypes}}. Her biri en fazla {{size}}MB.',
validation: {

@ -26,6 +26,8 @@ const translation = {
},
tryToSolve: 'Çözmeyi Dene',
temporarySystemIssue: 'Üzgünüz, geçici sistem sorunu.',
expand: 'Genişlet',
collapse: 'Kısıtla',
},
generation: {
tabs: {

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: 'Завантажити текстовий файл',
button: 'Перетягніть файл або',
button: 'Перетягніть файли або папки або',
browse: 'Оберіть',
tip: 'Підтримуються {{supportTypes}}. Максимум {{size}} МБ кожен.',
validation: {

@ -26,6 +26,8 @@ const translation = {
},
tryToSolve: 'Спробувати вирішити',
temporarySystemIssue: 'Вибачте, тимчасова системна проблема.',
expand: 'Розгорнути',
collapse: 'Згорнути',
},
generation: {
tabs: {

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: 'Tải lên tệp văn bản',
button: 'Kéo và thả tệp, hoặc',
button: 'Kéo và thả các tập tin hoặc thư mục, hoặc',
browse: 'Chọn tệp',
tip: 'Hỗ trợ {{supportTypes}}. Tối đa {{size}}MB mỗi tệp.',
validation: {

@ -26,6 +26,8 @@ const translation = {
},
tryToSolve: 'Thử giải quyết',
temporarySystemIssue: 'Xin lỗi, hệ thống đang gặp sự cố tạm thời.',
expand: 'Mở rộng',
collapse: 'Thu gọn',
},
generation: {
tabs: {

@ -35,7 +35,7 @@ const translation = {
},
uploader: {
title: '上传文本文件',
button: '拖拽文件至此,或者',
button: '拖拽文件或文件夹至此,或者',
browse: '选择文件',
tip: '已支持 {{supportTypes}},每个文件不超过 {{size}}MB。',
validation: {

@ -30,6 +30,8 @@ const translation = {
},
tryToSolve: '尝试解决',
temporarySystemIssue: '抱歉,临时系统问题。',
expand: '展开',
collapse: '折叠',
},
generation: {
tabs: {

@ -22,7 +22,7 @@ const translation = {
},
uploader: {
title: '上傳文字檔案',
button: '拖拽檔案至此,或者',
button: '拖拽檔案或檔案夾至此,或者',
browse: '選擇檔案',
tip: '已支援 {{supportTypes}},每個檔案不超過 {{size}}MB。',
validation: {

@ -26,6 +26,8 @@ const translation = {
},
tryToSolve: '嘗試解決',
temporarySystemIssue: '抱歉,臨時系統問題。',
expand: '展開',
collapse: '摺疊',
},
generation: {
tabs: {

@ -12,6 +12,7 @@
const buttonId = "dify-chatbot-bubble-button";
const iframeId = "dify-chatbot-bubble-window";
const config = window[configKey];
let isExpanded = false;
// SVG icons for open and close states
const svgIcons = `<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@ -22,6 +23,53 @@
</svg>
`;
const originalIframeStyleText = `
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
top: unset;
right: var(--${buttonId}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
bottom: var(--${buttonId}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
left: unset;
width: 24rem;
max-width: calc(100vw - 2rem);
height: 43.75rem;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
transition-property: width, height;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
`
const expandedIframeStyleText = `
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
top: unset;
right: var(--${buttonId}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
bottom: var(--${buttonId}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
left: unset;
min-width: 24rem;
width: 48%;
max-width: calc(100vw - 2rem);
min-height: 43.75rem;
height: 88%;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
transition-property: width, height;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
`
// Main function to embed the chatbot
async function embedChatbot() {
let isDragging = false
@ -71,6 +119,7 @@
const baseUrl =
config.baseUrl || `https://${config.isDev ? "dev." : ""}udify.app`;
const targetOrigin = new URL(baseUrl).origin;
// pre-check the length of the URL
const iframeUrl = `${baseUrl}/chatbot/${config.token}?${params}`;
@ -92,23 +141,7 @@
iframe.title = "dify chatbot bubble window";
iframe.id = iframeId;
iframe.src = iframeUrl;
iframe.style.cssText = `
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
left: unset;
right: 0;
bottom: 0;
width: 24rem;
max-width: calc(100vw - 2rem);
height: 43.75rem;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
`;
iframe.style.cssText = originalIframeStyleText;
return iframe;
}
@ -121,29 +154,70 @@
const targetButton = document.getElementById(buttonId);
if (targetIframe && targetButton) {
const buttonRect = targetButton.getBoundingClientRect();
// We don't necessarily need iframeRect anymore with the center logic
const buttonInBottom = buttonRect.top - 5 > targetIframe.clientHeight;
const viewportCenterY = window.innerHeight / 2;
const buttonCenterY = buttonRect.top + buttonRect.height / 2;
if (buttonInBottom) {
targetIframe.style.bottom = "0px";
targetIframe.style.top = "unset";
if (buttonCenterY < viewportCenterY) {
targetIframe.style.top = `var(--${buttonId}-bottom, 1rem)`;
targetIframe.style.bottom = 'unset';
} else {
targetIframe.style.bottom = "unset";
targetIframe.style.top = "0px";
targetIframe.style.bottom = `var(--${buttonId}-bottom, 1rem)`;
targetIframe.style.top = 'unset';
}
const buttonInRight = buttonRect.right > targetIframe.clientWidth;
const viewportCenterX = window.innerWidth / 2;
const buttonCenterX = buttonRect.left + buttonRect.width / 2;
if (buttonInRight) {
targetIframe.style.right = "0";
targetIframe.style.left = "unset";
if (buttonCenterX < viewportCenterX) {
targetIframe.style.left = `var(--${buttonId}-right, 1rem)`;
targetIframe.style.right = 'unset';
} else {
targetIframe.style.right = "unset";
targetIframe.style.left = 0;
targetIframe.style.right = `var(--${buttonId}-right, 1rem)`;
targetIframe.style.left = 'unset';
}
}
}
function toggleExpand() {
isExpanded = !isExpanded;
const targetIframe = document.getElementById(iframeId);
if (!targetIframe) return;
if (isExpanded) {
targetIframe.style.cssText = expandedIframeStyleText;
} else {
targetIframe.style.cssText = originalIframeStyleText;
}
resetIframePosition();
}
window.addEventListener('message', (event) => {
if (event.origin !== targetOrigin) return;
const targetIframe = document.getElementById(iframeId);
if (!targetIframe || event.source !== targetIframe.contentWindow) return;
if (event.data.type === 'dify-chatbot-iframe-ready') {
targetIframe.contentWindow?.postMessage(
{
type: 'dify-chatbot-config',
payload: {
isToggledByButton: true,
isDraggable: !!config.draggable,
},
},
targetOrigin
);
}
if (event.data.type === 'dify-chatbot-expand-change') {
toggleExpand();
}
});
// Function to create the chat button
function createButton() {
const containerDiv = document.createElement("div");

@ -1,20 +1,24 @@
(()=>{let t="difyChatbotConfig",m="dify-chatbot-bubble-button",h="dify-chatbot-bubble-window",p=window[t];async function e(){let u=!1;if(p&&p.token){var e=new URLSearchParams({...await(async()=>{var e=p?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await o(t)})),n})(),...await(async()=>{var e=p?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await o(t)})),n})()});let t=`${p.baseUrl||`https://${p.isDev?"dev.":""}udify.app`}/chatbot/${p.token}?`+e;e=s();async function o(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=h,e.src=t,e.style.cssText=`
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
left: unset;
right: 0;
bottom: 0;
width: 24rem;
max-width: calc(100vw - 2rem);
height: 43.75rem;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
`,e}function d(){var e,t;window.innerWidth<=640||(e=document.getElementById(h),t=document.getElementById(m),e&&t&&((t=t.getBoundingClientRect()).top-5>e.clientHeight?(e.style.bottom="0px",e.style.top="unset"):(e.style.bottom="unset",e.style.top="0px"),t.right>e.clientWidth?(e.style.right="0",e.style.left="unset"):(e.style.right="unset",e.style.left=0)))}function n(){let n=document.createElement("div");Object.entries(p.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=m;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(`
(()=>{let t="difyChatbotConfig",h="dify-chatbot-bubble-button",m="dify-chatbot-bubble-window",y=window[t],a=!1,l=`
position: absolute;
display: flex;
flex-direction: column;
justify-content: space-between;
top: unset;
right: var(--${h}-right, 1rem); /* Align with dify-chatbot-bubble-button. */
bottom: var(--${h}-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */
left: unset;
width: 24rem;
max-width: calc(100vw - 2rem);
height: 43.75rem;
max-height: calc(100vh - 6rem);
border: none;
z-index: 2147483640;
overflow: hidden;
user-select: none;
transition-property: width, height;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
`;async function e(){let u=!1;if(y&&y.token){var e=new URLSearchParams({...await(async()=>{var e=y?.inputs||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n[e]=await i(t)})),n})(),...await(async()=>{var e=y?.systemVariables||{};let n={};return await Promise.all(Object.entries(e).map(async([e,t])=>{n["sys."+e]=await i(t)})),n})()}),n=y.baseUrl||`https://${y.isDev?"dev.":""}udify.app`;let o=new URL(n).origin,t=`${n}/chatbot/${y.token}?`+e;n=s();async function i(e){e=(new TextEncoder).encode(e),e=new Response(new Blob([e]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer(),e=new Uint8Array(await e);return btoa(String.fromCharCode(...e))}function s(){var e=document.createElement("iframe");return e.allow="fullscreen;microphone",e.title="dify chatbot bubble window",e.id=m,e.src=t,e.style.cssText=l,e}function d(){var e,t,n;window.innerWidth<=640||(e=document.getElementById(m),t=document.getElementById(h),e&&t&&(t=t.getBoundingClientRect(),n=window.innerHeight/2,t.top+t.height/2<n?(e.style.top=`var(--${h}-bottom, 1rem)`,e.style.bottom="unset"):(e.style.bottom=`var(--${h}-bottom, 1rem)`,e.style.top="unset"),t.left+t.width/2<window.innerWidth/2?(e.style.left=`var(--${h}-right, 1rem)`,e.style.right="unset"):(e.style.right=`var(--${h}-right, 1rem)`,e.style.left="unset")))}function r(){let n=document.createElement("div");Object.entries(y.containerProps||{}).forEach(([e,t])=>{"className"===e?n.classList.add(...t.split(" ")):"style"===e?"object"==typeof t?Object.assign(n.style,t):n.style.cssText=t:"function"==typeof t?n.addEventListener(e.replace(/^on/,"").toLowerCase(),t):n[e]=t}),n.id=h;var e=document.createElement("style"),e=(document.head.appendChild(e),e.sheet.insertRule(`
#${n.id} {
position: fixed;
bottom: var(--${n.id}-bottom, 1rem);
@ -29,10 +33,10 @@
cursor: pointer;
z-index: 2147483647;
}
`),document.createElement("div"));function t(){var e;u||((e=document.getElementById(h))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?y("open"):y("close"),"none"===e.style.display?document.removeEventListener("keydown",l):document.addEventListener("keydown",l),d()):(n.appendChild(s()),d(),this.title="Exit (ESC)",y("close"),document.addEventListener("keydown",l)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
`),document.createElement("div"));function t(){var e;u||((e=document.getElementById(m))?(e.style.display="none"===e.style.display?"block":"none","none"===e.style.display?p("open"):p("close"),"none"===e.style.display?document.removeEventListener("keydown",b):document.addEventListener("keydown",b),d()):(n.appendChild(s()),d(),this.title="Exit (ESC)",p("close"),document.addEventListener("keydown",b)))}if(e.style.cssText="position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;",e.innerHTML=`<svg id="openIcon" 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="M7.7586 2L16.2412 2C17.0462 1.99999 17.7105 1.99998 18.2517 2.04419C18.8138 2.09012 19.3305 2.18868 19.8159 2.43598C20.5685 2.81947 21.1804 3.43139 21.5639 4.18404C21.8112 4.66937 21.9098 5.18608 21.9557 5.74818C21.9999 6.28937 21.9999 6.95373 21.9999 7.7587L22 14.1376C22.0004 14.933 22.0007 15.5236 21.8636 16.0353C21.4937 17.4156 20.4155 18.4938 19.0352 18.8637C18.7277 18.9461 18.3917 18.9789 17.9999 18.9918L17.9999 20.371C18 20.6062 18 20.846 17.9822 21.0425C17.9651 21.2305 17.9199 21.5852 17.6722 21.8955C17.3872 22.2525 16.9551 22.4602 16.4983 22.4597C16.1013 22.4593 15.7961 22.273 15.6386 22.1689C15.474 22.06 15.2868 21.9102 15.1031 21.7632L12.69 19.8327C12.1714 19.4178 12.0174 19.3007 11.8575 19.219C11.697 19.137 11.5262 19.0771 11.3496 19.0408C11.1737 19.0047 10.9803 19 10.3162 19H7.75858C6.95362 19 6.28927 19 5.74808 18.9558C5.18598 18.9099 4.66928 18.8113 4.18394 18.564C3.43129 18.1805 2.81937 17.5686 2.43588 16.816C2.18859 16.3306 2.09002 15.8139 2.0441 15.2518C1.99988 14.7106 1.99989 14.0463 1.9999 13.2413V7.75868C1.99989 6.95372 1.99988 6.28936 2.0441 5.74818C2.09002 5.18608 2.18859 4.66937 2.43588 4.18404C2.81937 3.43139 3.43129 2.81947 4.18394 2.43598C4.66928 2.18868 5.18598 2.09012 5.74808 2.04419C6.28927 1.99998 6.95364 1.99999 7.7586 2ZM10.5073 7.5C10.5073 6.67157 9.83575 6 9.00732 6C8.1789 6 7.50732 6.67157 7.50732 7.5C7.50732 8.32843 8.1789 9 9.00732 9C9.83575 9 10.5073 8.32843 10.5073 7.5ZM16.6073 11.7001C16.1669 11.3697 15.5426 11.4577 15.2105 11.8959C15.1488 11.9746 15.081 12.0486 15.0119 12.1207C14.8646 12.2744 14.6432 12.4829 14.3566 12.6913C13.7796 13.111 12.9818 13.5001 12.0073 13.5001C11.0328 13.5001 10.235 13.111 9.65799 12.6913C9.37138 12.4829 9.15004 12.2744 9.00274 12.1207C8.93366 12.0486 8.86581 11.9745 8.80418 11.8959C8.472 11.4577 7.84775 11.3697 7.40732 11.7001C6.96549 12.0314 6.87595 12.6582 7.20732 13.1001C7.20479 13.0968 7.21072 13.1043 7.22094 13.1171C7.24532 13.1478 7.29407 13.2091 7.31068 13.2289C7.36932 13.2987 7.45232 13.3934 7.55877 13.5045C7.77084 13.7258 8.08075 14.0172 8.48165 14.3088C9.27958 14.8891 10.4818 15.5001 12.0073 15.5001C13.5328 15.5001 14.735 14.8891 15.533 14.3088C15.9339 14.0172 16.2438 13.7258 16.4559 13.5045C16.5623 13.3934 16.6453 13.2987 16.704 13.2289C16.7333 13.1939 16.7567 13.165 16.7739 13.1432C17.1193 12.6969 17.0729 12.0493 16.6073 11.7001ZM15.0073 6C15.8358 6 16.5073 6.67157 16.5073 7.5C16.5073 8.32843 15.8358 9 15.0073 9C14.1789 9 13.5073 8.32843 13.5073 7.5C13.5073 6.67157 14.1789 6 15.0073 6Z" fill="white"/>
</svg>
<svg id="closeIcon" style="display:none" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 18L6 6M6 18L18 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",t),p.draggable){var r=n;var a=p.dragAxis||"both";let s,d,t,l;function o(e){u=!1,l=("touchstart"===e.type?(s=e.touches[0].clientX-r.offsetLeft,d=e.touches[0].clientY-r.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-r.offsetLeft,d=e.clientY-r.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-l;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){r.style.transition="none",r.style.cursor="grabbing";i=document.getElementById(h);i&&(i.style.display="none",y("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-d):(e=n.clientX-s,window.innerHeight-n.clientY-d);o=r.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==a&&"both"!==a||r.style.setProperty(`--${m}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==a&&"both"!==a||r.style.setProperty(`--${m}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),r.style.transition="",r.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}r.addEventListener("mousedown",o),r.addEventListener("touchstart",o)}}e.style.display="none",document.body.appendChild(e),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),document.getElementById(m)||n()}else console.error(t+" is empty or token is not provided")}function y(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function l(e){"Escape"===e.key&&(e=document.getElementById(h))&&"none"!==e.style.display&&(e.style.display="none",y("open"))}document.addEventListener("keydown",l),p?.dynamicScript?e():document.body.onload=e})();
`,n.appendChild(e),document.body.appendChild(n),n.addEventListener("click",t),n.addEventListener("touchend",t),y.draggable){var a=n;var l=y.dragAxis||"both";let s,d,t,r;function o(e){u=!1,r=("touchstart"===e.type?(s=e.touches[0].clientX-a.offsetLeft,d=e.touches[0].clientY-a.offsetTop,t=e.touches[0].clientX,e.touches[0]):(s=e.clientX-a.offsetLeft,d=e.clientY-a.offsetTop,t=e.clientX,e)).clientY,document.addEventListener("mousemove",i),document.addEventListener("touchmove",i,{passive:!1}),document.addEventListener("mouseup",c),document.addEventListener("touchend",c),e.preventDefault()}function i(n){var o="touchmove"===n.type?n.touches[0]:n,i=o.clientX-t,o=o.clientY-r;if(u=8<Math.abs(i)||8<Math.abs(o)?!0:u){a.style.transition="none",a.style.cursor="grabbing";i=document.getElementById(m);i&&(i.style.display="none",p("open"));let e,t;t="touchmove"===n.type?(e=n.touches[0].clientX-s,window.innerHeight-n.touches[0].clientY-d):(e=n.clientX-s,window.innerHeight-n.clientY-d);o=a.getBoundingClientRect(),i=window.innerWidth-o.width,n=window.innerHeight-o.height;"x"!==l&&"both"!==l||a.style.setProperty(`--${h}-left`,Math.max(0,Math.min(e,i))+"px"),"y"!==l&&"both"!==l||a.style.setProperty(`--${h}-bottom`,Math.max(0,Math.min(t,n))+"px")}}function c(){setTimeout(()=>{u=!1},0),a.style.transition="",a.style.cursor="pointer",document.removeEventListener("mousemove",i),document.removeEventListener("touchmove",i),document.removeEventListener("mouseup",c),document.removeEventListener("touchend",c)}a.addEventListener("mousedown",o),a.addEventListener("touchstart",o)}}n.style.display="none",document.body.appendChild(n),2048<t.length&&console.error("The URL is too long, please reduce the number of inputs to prevent the bot from failing to load"),window.addEventListener("message",e=>{var t,n;e.origin===o&&(t=document.getElementById(m))&&e.source===t.contentWindow&&("dify-chatbot-iframe-ready"===e.data.type&&t.contentWindow?.postMessage({type:"dify-chatbot-config",payload:{isToggledByButton:!0,isDraggable:!!y.draggable}},o),"dify-chatbot-expand-change"===e.data.type)&&(a=!a,n=document.getElementById(m))&&(a?n.style.cssText="\n position: absolute;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n top: unset;\n right: var(--dify-chatbot-bubble-button-right, 1rem); /* Align with dify-chatbot-bubble-button. */\n bottom: var(--dify-chatbot-bubble-button-bottom, 1rem); /* Align with dify-chatbot-bubble-button. */\n left: unset;\n min-width: 24rem;\n width: 48%;\n max-width: calc(100vw - 2rem);\n min-height: 43.75rem;\n height: 88%;\n max-height: calc(100vh - 6rem);\n border: none;\n z-index: 2147483640;\n overflow: hidden;\n user-select: none;\n transition-property: width, height;\n transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n transition-duration: 150ms;\n ":n.style.cssText=l,d())}),document.getElementById(h)||r()}else console.error(t+" is empty or token is not provided")}function p(e="open"){"open"===e?(document.getElementById("openIcon").style.display="block",document.getElementById("closeIcon").style.display="none"):(document.getElementById("openIcon").style.display="none",document.getElementById("closeIcon").style.display="block")}function b(e){"Escape"===e.key&&(e=document.getElementById(m))&&"none"!==e.style.display&&(e.style.display="none",p("open"))}h,h,document.addEventListener("keydown",b),y?.dynamicScript?e():document.body.onload=e})();
Loading…
Cancel
Save