Compare commits

..

44 Commits

Author SHA1 Message Date
e6f1500cfe move stopword data package from code to Dockerfile 2025-11-13 14:38:21 +08:00
4e201ef059 Merge branch 'main' of github.com:langgenius/dify into feat/no-root-image 2025-11-13 13:56:42 +08:00
6c576e2c66 add doc (#28016)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-13 13:38:45 +09:00
b0e7e7752f refactor(web): reuse the same edit-custom-collection-modal component, and fix the pop up error (#28003) 2025-11-13 11:44:21 +08:00
2799b79e8c fix: app's ai site text to speech api (#28091) 2025-11-13 11:44:04 +08:00
805a1479f9 fix: simplify graph structure validation in WorkflowService (#28146)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-13 10:59:31 +08:00
fe6538b08d chore: disable workflow logs auto-cleanup by default (#28136)
This PR changes the default value of `WORKFLOW_LOG_CLEANUP_ENABLED` from `true` to `false` across all configuration files.

## Motivation

Setting the default to `false` provides safer default behavior by:

- Preventing unintended data loss for new installations
- Giving users explicit control over when to enable log cleanup
- Following the opt-in principle for data deletion features

Users who need automatic cleanup can enable it by setting `WORKFLOW_LOG_CLEANUP_ENABLED=true` in their configuration.
2025-11-12 22:55:02 +08:00
1bbb9d6644 convert to TypeBase (#27935)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 21:50:13 +08:00
5c06e285ec test: create some hooks and utils test script, modified clipboard test script (#27928) 2025-11-12 21:47:06 +08:00
19c92fd670 Add file type validation to paste upload (#28017) 2025-11-12 19:27:56 +08:00
6026bd873b fix: variable assigner can't assign float number (#28068) 2025-11-12 19:27:36 +08:00
1369119a0c fix: determine cpu cores determination in baseedpyright-check script on macos (#28058) 2025-11-12 19:27:27 +08:00
b76e17b25d feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
Co-authored-by: Harry <xh001x@hotmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: WTW0313 <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-12 17:59:37 +08:00
ca7794305b add transform-datasource-credentials command online check (#28124)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Garfield Dai <dai.hai@foxmail.com>
2025-11-12 17:13:44 +08:00
fd255e81e1 feat(api): Introduce WorkflowResumptionContext for pause state management (#28122)
Certain metadata (including but not limited to `InvokeFrom`, `call_depth`, and `streaming`)  is required when resuming a paused workflow. However, these fields are not part of `GraphRuntimeState` and were not saved in the previous
 implementation of  `PauseStatePersistenceLayer`.

This commit addresses this limitation by introducing a `WorkflowResumptionContext` model that wraps both the `*GenerateEntity` and `GraphRuntimeState`. This approach provides:

- A structured container for all necessary resumption data
- Better separation of concerns between execution state and persistence
- Enhanced extensibility for future metadata additions
- Clearer naming that distinguishes from `GraphRuntimeState`

The `WorkflowResumptionContext` model makes extending the pause state easier while maintaining backward compatibility and proper version management for the entire execution state ecosystem.

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-11-12 17:00:02 +08:00
09d31d1263 chore: improve the user experience of not login into apps (#28120) 2025-11-12 16:47:45 +08:00
47dc26f011 fix document index test (#28113) 2025-11-12 16:00:10 +08:00
123bb3ec08 When graph_engine worker run exception, keep the node_id for deep res… (#26205)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2025-11-12 15:03:45 +08:00
90f77282e3 chore: not SaaS version can query long log time range (#28109) 2025-11-12 14:45:56 +08:00
5208867ccc fix document enable (#28081) 2025-11-11 17:50:45 +08:00
edc7ccc795 chore: add type-check to pre-commit (#28005) 2025-11-11 16:14:39 +08:00
c9798f6425 fix(api): Trace Hierarchy, Span Status, and Broken Workflow for Arize & Phoenix Integration (#27937)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
2025-11-11 11:49:19 +08:00
20ecf7f1d0 chore: remove unused enterprise bot from the readme (#28073)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-11 10:52:27 +08:00
9dcb780fcb chore: translate i18n files and update type definitions (#28054)
Co-authored-by: iamjoel <2120155+iamjoel@users.noreply.github.com>
2025-11-11 09:32:53 +08:00
1cb7b09933 chore: Remove trailing space from migration filename (#28040) 2025-11-11 09:32:42 +08:00
2c62a77cf4 Chore: change query log time range (#28052)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-10 18:39:12 +08:00
b9bc48d8dd feat(api): Introduce Broadcast Channel (#27835)
This PR introduces a `BroadcastChannel` abstraction with broadcasting and at-most once delivery semantics, serving as the communication component between celery worker and API server.

It also includes a reference implementation backed by Redis PubSub.

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-11-10 17:23:21 +08:00
ed234e311b fix workflow default updated_at (#28047) 2025-11-10 18:20:38 +09:00
9843fec393 fix: elasticsearch_vector version (#28028)
Co-authored-by: huangzhuo <huangzhuo1@xiaomi.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-10 13:17:13 +09:00
aa4cabdeb5 feat: Add Audio Content Support for MCP Tools (#27979) 2025-11-10 10:12:11 +08:00
eea713b668 Fix typo in weaviate comment, improve time test precision, and add security tests for get-icon utility (#27919)
Signed-off-by: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-10 10:11:54 +08:00
fc62538a94 chore(deps): bump scipy-stubs from 1.16.2.3 to 1.16.3.0 in /api (#28025)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 09:54:56 +08:00
7994144df7 add onupdate=func.current_timestamp() (#28014)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-10 01:48:52 +09:00
e153c483b6 fix: the model list encountered two children with the same key (#27956)
Co-authored-by: haokai <haokai@shuwen.com>
2025-11-09 21:39:59 +08:00
422bb4d4bb fix: fix https://github.com/langgenius/dify/issues/27939 (#27985) 2025-11-09 21:39:05 +08:00
87a80d7613 docs: clarify how to obtain workflow_id for version execution (#28007)
Signed-off-by: OneZero-Y <aukovyps@163.com>
2025-11-09 21:38:06 +08:00
e91105ca87 fix: bump brotli to 1.2.0 resloved CVE-2025-6176 (#27950)
Signed-off-by: kenwoodjw <blackxin55+@gmail.com>
2025-11-07 15:57:29 +08:00
37903722fe refactor: implement tenant self queue for rag tasks (#27559)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
2025-11-06 21:25:50 +08:00
f4c82d0010 fix(api): fix VariablePool.get adding unexpected keys to variable_dictionary (#26767)
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-06 18:30:35 +08:00
fe50093c18 fix: prevent fetch version info in enterprise edition (#27923) 2025-11-06 17:59:53 +08:00
4317af1e90 fix jina reader transform (#27922) 2025-11-06 17:35:53 +08:00
bf73c6036f xMerge branch 'main' of github.com:langgenius/dify into feat/no-root-image 2025-09-29 14:21:45 +08:00
3d69f715b9 update dockerfile to fix nltk data download dir path 2025-09-29 14:05:57 +08:00
cda2a698ed use no-root user in docker image by default 2025-09-29 13:26:45 +08:00
103 changed files with 3326 additions and 10827 deletions

View File

@ -6,10 +6,11 @@ cd web && pnpm install
pipx install uv
echo "alias start-api=\"cd $WORKSPACE_ROOT/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug\"" >> ~/.bashrc
echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,priority_dataset,priority_pipeline,pipeline,mail,ops_trace,app_deletion,plugin,workflow_storage,conversation,workflow,schedule_poller,schedule_executor,triggered_workflow_dispatcher,trigger_refresh_executor\"" >> ~/.bashrc
echo "alias start-worker=\"cd $WORKSPACE_ROOT/api && uv run python -m celery -A app.celery worker -P threads -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion,plugin,workflow_storage\"" >> ~/.bashrc
echo "alias start-web=\"cd $WORKSPACE_ROOT/web && pnpm dev\"" >> ~/.bashrc
echo "alias start-web-prod=\"cd $WORKSPACE_ROOT/web && pnpm build && pnpm start\"" >> ~/.bashrc
echo "alias start-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d\"" >> ~/.bashrc
echo "alias stop-containers=\"cd $WORKSPACE_ROOT/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down\"" >> ~/.bashrc
source /home/vscode/.bashrc

View File

@ -73,7 +73,8 @@ COPY --from=packages ${VIRTUAL_ENV} ${VIRTUAL_ENV}
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}"
# Download nltk data
RUN python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger')"
RUN mkdir -p /usr/local/share/nltk_data && NLTK_DATA=/usr/local/share/nltk_data python -c "import nltk; nltk.download('punkt'); nltk.download('averaged_perceptron_tagger'); nltk.download('stopwords')" \
&& chmod -R 755 /usr/local/share/nltk_data
ENV TIKTOKEN_CACHE_DIR=/app/api/.tiktoken_cache
@ -86,7 +87,15 @@ COPY . /app/api/
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Create non-root user and set permissions
RUN groupadd -r -g 1001 dify && \
useradd -r -u 1001 -g 1001 -s /bin/bash dify && \
mkdir -p /home/dify && \
chown -R 1001:1001 /app /home/dify ${TIKTOKEN_CACHE_DIR} /entrypoint.sh
ARG COMMIT_SHA
ENV COMMIT_SHA=${COMMIT_SHA}
ENV NLTK_DATA=/usr/local/share/nltk_data
USER 1001
ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]

File diff suppressed because one or more lines are too long

View File

@ -15,12 +15,11 @@ from controllers.console.wraps import (
setup_required,
)
from core.ops.ops_trace_manager import OpsTraceManager
from core.workflow.enums import NodeType
from extensions.ext_database import db
from fields.app_fields import app_detail_fields, app_detail_fields_with_site, app_pagination_fields
from libs.login import current_account_with_tenant, login_required
from libs.validators import validate_description_length
from models import App, Workflow
from models import App
from services.app_dsl_service import AppDslService, ImportMode
from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService
@ -107,35 +106,6 @@ class AppListApi(Resource):
if str(app.id) in res:
app.access_mode = res[str(app.id)].access_mode
workflow_capable_app_ids = [
str(app.id) for app in app_pagination.items if app.mode in {"workflow", "advanced-chat"}
]
draft_trigger_app_ids: set[str] = set()
if workflow_capable_app_ids:
draft_workflows = (
db.session.execute(
select(Workflow).where(
Workflow.version == Workflow.VERSION_DRAFT,
Workflow.app_id.in_(workflow_capable_app_ids),
)
)
.scalars()
.all()
)
trigger_node_types = {
NodeType.TRIGGER_WEBHOOK,
NodeType.TRIGGER_SCHEDULE,
NodeType.TRIGGER_PLUGIN,
}
for workflow in draft_workflows:
for _, node_data in workflow.walk_nodes():
if node_data.get("type") in trigger_node_types:
draft_trigger_app_ids.add(str(workflow.app_id))
break
for app in app_pagination.items:
app.has_draft_trigger = str(app.id) in draft_trigger_app_ids
return marshal(app_pagination, app_pagination_fields), 200
@api.doc("create_app")

View File

@ -138,10 +138,6 @@ class StreamableHTTPTransport:
) -> bool:
"""Handle an SSE event, returning True if the response is complete."""
if sse.event == "message":
# ping event send by server will be recognized as a message event with empty data by httpx-sse's SSEDecoder
if not sse.data.strip():
return False
try:
message = JSONRPCMessage.model_validate_json(sse.data)
logger.debug("SSE message: %s", message)

View File

@ -52,7 +52,7 @@ class OpenAIModeration(Moderation):
text = "\n".join(str(inputs.values()))
model_manager = ModelManager()
model_instance = model_manager.get_model_instance(
tenant_id=self.tenant_id, provider="openai", model_type=ModelType.MODERATION, model="omni-moderation-latest"
tenant_id=self.tenant_id, provider="openai", model_type=ModelType.MODERATION, model="text-moderation-stable"
)
openai_moderation = model_instance.invoke_moderation(text=text)

View File

@ -302,8 +302,7 @@ class OracleVector(BaseVector):
nltk.data.find("tokenizers/punkt")
nltk.data.find("corpora/stopwords")
except LookupError:
nltk.download("punkt")
nltk.download("stopwords")
raise LookupError("Unable to find the required NLTK data package: punkt and stopwords")
e_str = re.sub(r"[^\w ]", "", query)
all_tokens = nltk.word_tokenize(e_str)
stop_words = stopwords.words("english")

View File

@ -152,15 +152,13 @@ class WordExtractor(BaseExtractor):
# Initialize a row, all of which are empty by default
row_cells = [""] * total_cols
col_index = 0
while col_index < len(row.cells):
for cell in row.cells:
# make sure the col_index is not out of range
while col_index < len(row.cells) and row_cells[col_index] != "":
while col_index < total_cols and row_cells[col_index] != "":
col_index += 1
# if col_index is out of range the loop is jumped
if col_index >= len(row.cells):
if col_index >= total_cols:
break
# get the correct cell
cell = row.cells[col_index]
cell_content = self._parse_cell(cell, image_map).strip()
cell_colspan = cell.grid_span or 1
for i in range(cell_colspan):

View File

@ -54,9 +54,6 @@ class TenantIsolatedTaskQueue:
serialized_data = wrapper.serialize()
serialized_tasks.append(serialized_data)
if not serialized_tasks:
return
redis_client.lpush(self._queue, *serialized_tasks)
def pull_tasks(self, count: int = 1) -> Sequence[Any]:

View File

@ -202,35 +202,6 @@ class SegmentType(StrEnum):
raise ValueError(f"element_type is only supported by array type, got {self}")
return _ARRAY_ELEMENT_TYPES_MAPPING.get(self)
@staticmethod
def get_zero_value(t: "SegmentType"):
# Lazy import to avoid circular dependency
from factories import variable_factory
match t:
case (
SegmentType.ARRAY_OBJECT
| SegmentType.ARRAY_ANY
| SegmentType.ARRAY_STRING
| SegmentType.ARRAY_NUMBER
| SegmentType.ARRAY_BOOLEAN
):
return variable_factory.build_segment_with_type(t, [])
case SegmentType.OBJECT:
return variable_factory.build_segment({})
case SegmentType.STRING:
return variable_factory.build_segment("")
case SegmentType.INTEGER:
return variable_factory.build_segment(0)
case SegmentType.FLOAT:
return variable_factory.build_segment(0.0)
case SegmentType.NUMBER:
return variable_factory.build_segment(0)
case SegmentType.BOOLEAN:
return variable_factory.build_segment(False)
case _:
raise ValueError(f"unsupported variable type: {t}")
_ARRAY_ELEMENT_TYPES_MAPPING: Mapping[SegmentType, SegmentType] = {
# ARRAY_ANY does not have corresponding element type.

View File

@ -2,6 +2,7 @@ from collections.abc import Callable, Mapping, Sequence
from typing import TYPE_CHECKING, Any, TypeAlias
from core.variables import SegmentType, Variable
from core.variables.segments import BooleanSegment
from core.workflow.constants import CONVERSATION_VARIABLE_NODE_ID
from core.workflow.conversation_variable_updater import ConversationVariableUpdater
from core.workflow.entities import GraphInitParams
@ -11,6 +12,7 @@ from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig
from core.workflow.nodes.base.node import Node
from core.workflow.nodes.variable_assigner.common import helpers as common_helpers
from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNodeError
from factories import variable_factory
from ..common.impl import conversation_variable_updater_factory
from .node_data import VariableAssignerData, WriteMode
@ -114,7 +116,7 @@ class VariableAssignerNode(Node):
updated_variable = original_variable.model_copy(update={"value": updated_value})
case WriteMode.CLEAR:
income_value = SegmentType.get_zero_value(original_variable.value_type)
income_value = get_zero_value(original_variable.value_type)
updated_variable = original_variable.model_copy(update={"value": income_value.to_object()})
# Over write the variable.
@ -141,3 +143,24 @@ class VariableAssignerNode(Node):
process_data=common_helpers.set_updated_variables({}, updated_variables),
outputs={},
)
def get_zero_value(t: SegmentType):
# TODO(QuantumGhost): this should be a method of `SegmentType`.
match t:
case SegmentType.ARRAY_OBJECT | SegmentType.ARRAY_STRING | SegmentType.ARRAY_NUMBER | SegmentType.ARRAY_BOOLEAN:
return variable_factory.build_segment_with_type(t, [])
case SegmentType.OBJECT:
return variable_factory.build_segment({})
case SegmentType.STRING:
return variable_factory.build_segment("")
case SegmentType.INTEGER:
return variable_factory.build_segment(0)
case SegmentType.FLOAT:
return variable_factory.build_segment(0.0)
case SegmentType.NUMBER:
return variable_factory.build_segment(0)
case SegmentType.BOOLEAN:
return BooleanSegment(value=False)
case _:
raise VariableOperatorNodeError(f"unsupported variable type: {t}")

View File

@ -0,0 +1,14 @@
from core.variables import SegmentType
# Note: This mapping is duplicated with `get_zero_value`. Consider refactoring to avoid redundancy.
EMPTY_VALUE_MAPPING = {
SegmentType.STRING: "",
SegmentType.NUMBER: 0,
SegmentType.BOOLEAN: False,
SegmentType.OBJECT: {},
SegmentType.ARRAY_ANY: [],
SegmentType.ARRAY_STRING: [],
SegmentType.ARRAY_NUMBER: [],
SegmentType.ARRAY_OBJECT: [],
SegmentType.ARRAY_BOOLEAN: [],
}

View File

@ -16,6 +16,7 @@ from core.workflow.nodes.variable_assigner.common.exc import VariableOperatorNod
from core.workflow.nodes.variable_assigner.common.impl import conversation_variable_updater_factory
from . import helpers
from .constants import EMPTY_VALUE_MAPPING
from .entities import VariableAssignerNodeData, VariableOperationItem
from .enums import InputType, Operation
from .exc import (
@ -248,7 +249,7 @@ class VariableAssignerNode(Node):
case Operation.OVER_WRITE:
return value
case Operation.CLEAR:
return SegmentType.get_zero_value(variable.value_type).to_object()
return EMPTY_VALUE_MAPPING[variable.value_type]
case Operation.APPEND:
return variable.value + [value]
case Operation.EXTEND:

View File

@ -3,7 +3,7 @@ import io
import json
from collections.abc import Generator
from google.cloud import storage as google_cloud_storage # type: ignore
from google.cloud import storage as google_cloud_storage
from configs import dify_config
from extensions.storage.base_storage import BaseStorage

View File

@ -116,7 +116,6 @@ app_partial_fields = {
"access_mode": fields.String,
"create_user_name": fields.String,
"author_name": fields.String,
"has_draft_trigger": fields.Boolean,
}

View File

@ -21,7 +21,6 @@ from configs import dify_config
from core.rag.index_processor.constant.built_in_field import BuiltInField, MetadataDataSource
from core.rag.retrieval.retrieval_methods import RetrievalMethod
from extensions.ext_storage import storage
from models.base import TypeBase
from services.entities.knowledge_entities.knowledge_entities import ParentMode, Rule
from .account import Account
@ -907,21 +906,17 @@ class ChildChunk(Base):
return db.session.query(DocumentSegment).where(DocumentSegment.id == self.segment_id).first()
class AppDatasetJoin(TypeBase):
class AppDatasetJoin(Base):
__tablename__ = "app_dataset_joins"
__table_args__ = (
sa.PrimaryKeyConstraint("id", name="app_dataset_join_pkey"),
sa.Index("app_dataset_join_app_dataset_idx", "dataset_id", "app_id"),
)
id: Mapped[str] = mapped_column(
StringUUID, primary_key=True, nullable=False, server_default=sa.text("uuid_generate_v4()"), init=False
)
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
dataset_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=sa.func.current_timestamp(), init=False
)
id = mapped_column(StringUUID, primary_key=True, nullable=False, server_default=sa.text("uuid_generate_v4()"))
app_id = mapped_column(StringUUID, nullable=False)
dataset_id = mapped_column(StringUUID, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=sa.func.current_timestamp())
@property
def app(self):

View File

@ -1,6 +1,6 @@
[project]
name = "dify-api"
version = "1.10.0"
version = "1.9.2"
requires-python = ">=3.11,<3.13"
dependencies = [
@ -37,7 +37,7 @@ dependencies = [
"numpy~=1.26.4",
"openpyxl~=3.1.5",
"opik~=1.8.72",
"litellm==1.77.1", # Pinned to avoid madoka dependency issue
"litellm==1.77.1", # Pinned to avoid madoka dependency issue
"opentelemetry-api==1.27.0",
"opentelemetry-distro==0.48b0",
"opentelemetry-exporter-otlp==1.27.0",
@ -79,6 +79,7 @@ dependencies = [
"tiktoken~=0.9.0",
"transformers~=4.56.1",
"unstructured[docx,epub,md,ppt,pptx]~=0.16.1",
"weave~=0.51.0",
"yarl~=1.18.3",
"webvtt-py~=0.5.1",
"sseclient-py~=1.8.0",
@ -89,7 +90,6 @@ dependencies = [
"croniter>=6.0.0",
"weaviate-client==4.17.0",
"apscheduler>=3.11.0",
"weave>=0.52.16",
]
# Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group.

View File

@ -1,5 +1,5 @@
import json
from typing import Any, TypedDict
from typing import Any
from core.app.app_config.entities import (
DatasetEntity,
@ -28,12 +28,6 @@ from models.model import App, AppMode, AppModelConfig
from models.workflow import Workflow, WorkflowType
class _NodeType(TypedDict):
id: str
position: None
data: dict[str, Any]
class WorkflowConverter:
"""
App Convert to Workflow Mode
@ -223,7 +217,7 @@ class WorkflowConverter:
return app_config
def _convert_to_start_node(self, variables: list[VariableEntity]) -> _NodeType:
def _convert_to_start_node(self, variables: list[VariableEntity]):
"""
Convert to Start Node
:param variables: list of variables
@ -241,7 +235,7 @@ class WorkflowConverter:
def _convert_to_http_request_node(
self, app_model: App, variables: list[VariableEntity], external_data_variables: list[ExternalDataVariableEntity]
) -> tuple[list[_NodeType], dict[str, str]]:
) -> tuple[list[dict], dict[str, str]]:
"""
Convert API Based Extension to HTTP Request Node
:param app_model: App instance
@ -291,7 +285,7 @@ class WorkflowConverter:
request_body_json = json.dumps(request_body)
request_body_json = request_body_json.replace(r"\{\{", "{{").replace(r"\}\}", "}}")
http_request_node: _NodeType = {
http_request_node = {
"id": f"http_request_{index}",
"position": None,
"data": {
@ -309,7 +303,7 @@ class WorkflowConverter:
nodes.append(http_request_node)
# append code node for response body parsing
code_node: _NodeType = {
code_node: dict[str, Any] = {
"id": f"code_{index}",
"position": None,
"data": {
@ -332,7 +326,7 @@ class WorkflowConverter:
def _convert_to_knowledge_retrieval_node(
self, new_app_mode: AppMode, dataset_config: DatasetEntity, model_config: ModelConfigEntity
) -> _NodeType | None:
) -> dict | None:
"""
Convert datasets to Knowledge Retrieval Node
:param new_app_mode: new app mode
@ -390,7 +384,7 @@ class WorkflowConverter:
prompt_template: PromptTemplateEntity,
file_upload: FileUploadConfig | None = None,
external_data_variable_node_mapping: dict[str, str] | None = None,
) -> _NodeType:
):
"""
Convert to LLM Node
:param original_app_mode: original app mode
@ -567,7 +561,7 @@ class WorkflowConverter:
return template
def _convert_to_end_node(self) -> _NodeType:
def _convert_to_end_node(self):
"""
Convert to End Node
:return:
@ -583,7 +577,7 @@ class WorkflowConverter:
},
}
def _convert_to_answer_node(self) -> _NodeType:
def _convert_to_answer_node(self):
"""
Convert to Answer Node
:return:
@ -604,7 +598,7 @@ class WorkflowConverter:
"""
return {"id": f"{source}-{target}", "source": source, "target": target}
def _append_node(self, graph: dict[str, Any], node: _NodeType):
def _append_node(self, graph: dict, node: dict):
"""
Append Node to Graph

View File

@ -1,49 +0,0 @@
"""Primarily used for testing merged cell scenarios"""
from docx import Document
from core.rag.extractor.word_extractor import WordExtractor
def _generate_table_with_merged_cells():
doc = Document()
"""
The table looks like this:
+-----+-----+-----+
| 1-1 & 1-2 | 1-3 |
+-----+-----+-----+
| 2-1 | 2-2 | 2-3 |
| & |-----+-----+
| 3-1 | 3-2 | 3-3 |
+-----+-----+-----+
"""
table = doc.add_table(rows=3, cols=3)
table.style = "Table Grid"
for i in range(3):
for j in range(3):
cell = table.cell(i, j)
cell.text = f"{i + 1}-{j + 1}"
# Merge cells
cell_0_0 = table.cell(0, 0)
cell_0_1 = table.cell(0, 1)
merged_cell_1 = cell_0_0.merge(cell_0_1)
merged_cell_1.text = "1-1 & 1-2"
cell_1_0 = table.cell(1, 0)
cell_2_0 = table.cell(2, 0)
merged_cell_2 = cell_1_0.merge(cell_2_0)
merged_cell_2.text = "2-1 & 3-1"
ground_truth = [["1-1 & 1-2", "", "1-3"], ["2-1 & 3-1", "2-2", "2-3"], ["2-1 & 3-1", "3-2", "3-3"]]
return doc.tables[0], ground_truth
def test_parse_row():
table, gt = _generate_table_with_merged_cells()
extractor = object.__new__(WordExtractor)
for idx, row in enumerate(table.rows):
assert extractor._parse_row(row, {}, 3) == gt[idx]

View File

@ -179,7 +179,7 @@ class TestTenantIsolatedTaskQueue:
"""Test pushing empty task list."""
sample_queue.push_tasks([])
mock_redis.lpush.assert_not_called()
mock_redis.lpush.assert_called_once_with("tenant_self_test-key_task_queue:tenant-123")
@patch("core.rag.pipeline.queue.redis_client")
def test_pull_tasks_default_count(self, mock_redis, sample_queue):

View File

@ -1,5 +1,3 @@
import pytest
from core.variables.types import ArrayValidation, SegmentType
@ -85,81 +83,3 @@ class TestSegmentTypeIsValidArrayValidation:
value = [1, 2, 3]
# validation is None, skip
assert SegmentType.ARRAY_STRING.is_valid(value, array_validation=ArrayValidation.NONE)
class TestSegmentTypeGetZeroValue:
"""
Test class for SegmentType.get_zero_value static method.
Provides comprehensive coverage of all supported SegmentType values to ensure
correct zero value generation for each type.
"""
def test_array_types_return_empty_list(self):
"""Test that all array types return empty list segments."""
array_types = [
SegmentType.ARRAY_ANY,
SegmentType.ARRAY_STRING,
SegmentType.ARRAY_NUMBER,
SegmentType.ARRAY_OBJECT,
SegmentType.ARRAY_BOOLEAN,
]
for seg_type in array_types:
result = SegmentType.get_zero_value(seg_type)
assert result.value == []
assert result.value_type == seg_type
def test_object_returns_empty_dict(self):
"""Test that OBJECT type returns empty dictionary segment."""
result = SegmentType.get_zero_value(SegmentType.OBJECT)
assert result.value == {}
assert result.value_type == SegmentType.OBJECT
def test_string_returns_empty_string(self):
"""Test that STRING type returns empty string segment."""
result = SegmentType.get_zero_value(SegmentType.STRING)
assert result.value == ""
assert result.value_type == SegmentType.STRING
def test_integer_returns_zero(self):
"""Test that INTEGER type returns zero segment."""
result = SegmentType.get_zero_value(SegmentType.INTEGER)
assert result.value == 0
assert result.value_type == SegmentType.INTEGER
def test_float_returns_zero_point_zero(self):
"""Test that FLOAT type returns 0.0 segment."""
result = SegmentType.get_zero_value(SegmentType.FLOAT)
assert result.value == 0.0
assert result.value_type == SegmentType.FLOAT
def test_number_returns_zero(self):
"""Test that NUMBER type returns zero segment."""
result = SegmentType.get_zero_value(SegmentType.NUMBER)
assert result.value == 0
# NUMBER type with integer value returns INTEGER segment type
# (NUMBER is a union type that can be INTEGER or FLOAT)
assert result.value_type == SegmentType.INTEGER
# Verify that exposed_type returns NUMBER for frontend compatibility
assert result.value_type.exposed_type() == SegmentType.NUMBER
def test_boolean_returns_false(self):
"""Test that BOOLEAN type returns False segment."""
result = SegmentType.get_zero_value(SegmentType.BOOLEAN)
assert result.value is False
assert result.value_type == SegmentType.BOOLEAN
def test_unsupported_types_raise_value_error(self):
"""Test that unsupported types raise ValueError."""
unsupported_types = [
SegmentType.SECRET,
SegmentType.FILE,
SegmentType.NONE,
SegmentType.GROUP,
SegmentType.ARRAY_FILE,
]
for seg_type in unsupported_types:
with pytest.raises(ValueError, match="unsupported variable type"):
SegmentType.get_zero_value(seg_type)

View File

@ -1,46 +0,0 @@
"""
Utilities for detecting if database service is available for workflow tests.
"""
import psycopg2
import pytest
from configs import dify_config
def is_database_available() -> bool:
"""
Check if the database service is available by attempting to connect to it.
Returns:
True if database is available, False otherwise.
"""
try:
# Try to establish a database connection using a context manager
with psycopg2.connect(
host=dify_config.DB_HOST,
port=dify_config.DB_PORT,
database=dify_config.DB_DATABASE,
user=dify_config.DB_USERNAME,
password=dify_config.DB_PASSWORD,
connect_timeout=2, # 2 second timeout
) as conn:
pass # Connection established and will be closed automatically
return True
except (psycopg2.OperationalError, psycopg2.Error):
return False
def skip_if_database_unavailable():
"""
Pytest skip decorator that skips tests when database service is unavailable.
Usage:
@skip_if_database_unavailable()
def test_my_workflow():
...
"""
return pytest.mark.skipif(
not is_database_available(),
reason="Database service is not available (connection refused or authentication failed)",
)

View File

@ -6,11 +6,9 @@ This module tests the iteration node's ability to:
2. Preserve nested array structure when flatten_output=False
"""
from .test_database_utils import skip_if_database_unavailable
from .test_table_runner import TableTestRunner, WorkflowTestCase
@skip_if_database_unavailable()
def test_iteration_with_flatten_output_enabled():
"""
Test iteration node with flatten_output=True (default behavior).
@ -39,7 +37,6 @@ def test_iteration_with_flatten_output_enabled():
)
@skip_if_database_unavailable()
def test_iteration_with_flatten_output_disabled():
"""
Test iteration node with flatten_output=False.
@ -68,7 +65,6 @@ def test_iteration_with_flatten_output_disabled():
)
@skip_if_database_unavailable()
def test_iteration_flatten_output_comparison():
"""
Run both flatten_output configurations in parallel to verify the difference.

View File

@ -199,7 +199,6 @@ def test__convert_to_knowledge_retrieval_node_for_chatbot():
node = WorkflowConverter()._convert_to_knowledge_retrieval_node(
new_app_mode=new_app_mode, dataset_config=dataset_config, model_config=model_config
)
assert node is not None
assert node["data"]["type"] == "knowledge-retrieval"
assert node["data"]["query_variable_selector"] == ["sys", "query"]
@ -232,7 +231,6 @@ def test__convert_to_knowledge_retrieval_node_for_workflow_app():
node = WorkflowConverter()._convert_to_knowledge_retrieval_node(
new_app_mode=new_app_mode, dataset_config=dataset_config, model_config=model_config
)
assert node is not None
assert node["data"]["type"] == "knowledge-retrieval"
assert node["data"]["query_variable_selector"] == ["start", dataset_config.retrieve_config.query_variable]

1598
api/uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.10.0
image: langgenius/dify-api:1.10.0-rc1
restart: always
environment:
# Use the shared environment variables.
@ -31,7 +31,7 @@ services:
# worker service
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
worker:
image: langgenius/dify-api:1.10.0
image: langgenius/dify-api:1.10.0-rc1
restart: always
environment:
# Use the shared environment variables.
@ -58,7 +58,7 @@ services:
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.10.0
image: langgenius/dify-api:1.10.0-rc1
restart: always
environment:
# Use the shared environment variables.
@ -76,7 +76,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.10.0
image: langgenius/dify-web:1.10.0-rc1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -182,7 +182,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.4.1-local
image: langgenius/dify-plugin-daemon:0.4.0-local
restart: always
environment:
# Use the shared environment variables.

View File

@ -625,7 +625,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.10.0
image: langgenius/dify-api:1.10.0-rc1
restart: always
environment:
# Use the shared environment variables.
@ -654,7 +654,7 @@ services:
# worker service
# The Celery worker for processing all queues (dataset, workflow, mail, etc.)
worker:
image: langgenius/dify-api:1.10.0
image: langgenius/dify-api:1.10.0-rc1
restart: always
environment:
# Use the shared environment variables.
@ -681,7 +681,7 @@ services:
# worker_beat service
# Celery beat for scheduling periodic tasks.
worker_beat:
image: langgenius/dify-api:1.10.0
image: langgenius/dify-api:1.10.0-rc1
restart: always
environment:
# Use the shared environment variables.
@ -699,7 +699,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.10.0
image: langgenius/dify-web:1.10.0-rc1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -805,7 +805,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.4.1-local
image: langgenius/dify-plugin-daemon:0.4.0-local
restart: always
environment:
# Use the shared environment variables.

View File

@ -37,22 +37,18 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
const isWorkflowApp = appDetail?.mode === AppModeEnum.WORKFLOW
const showMCPCard = isInPanel
const showTriggerCard = isInPanel && isWorkflowApp
const { data: currentWorkflow } = useAppWorkflow(isWorkflowApp ? appDetail.id : '')
const hasTriggerNode = useMemo<boolean | null>(() => {
if (!isWorkflowApp)
const showTriggerCard = isInPanel && appDetail?.mode === AppModeEnum.WORKFLOW
const { data: currentWorkflow } = useAppWorkflow(appDetail?.mode === AppModeEnum.WORKFLOW ? appDetail.id : '')
const hasTriggerNode = useMemo(() => {
if (appDetail?.mode !== AppModeEnum.WORKFLOW)
return false
if (!currentWorkflow)
return null
const nodes = currentWorkflow.graph?.nodes || []
const nodes = currentWorkflow?.graph?.nodes || []
return nodes.some((node) => {
const nodeType = node.data?.type as BlockEnum | undefined
return !!nodeType && isTriggerNode(nodeType)
})
}, [isWorkflowApp, currentWorkflow])
const shouldRenderAppCards = !isWorkflowApp || hasTriggerNode === false
}, [appDetail?.mode, currentWorkflow])
const updateAppDetail = async () => {
try {
@ -127,7 +123,7 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
return (
<div className={className || 'mb-6 grid w-full grid-cols-1 gap-6 xl:grid-cols-2'}>
{
shouldRenderAppCards && (
!hasTriggerNode && (
<>
<AppCard
appInfo={appDetail}

View File

@ -3,7 +3,6 @@ import type { ReactNode } from 'react'
import SwrInitializer from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga'
import AmplitudeProvider from '@/app/components/base/amplitude'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import Header from '@/app/components/header'
import { EventEmitterContextProvider } from '@/context/event-emitter'
@ -18,7 +17,6 @@ const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitializer>
<AppContextProvider>
<EventEmitterContextProvider>

View File

@ -4,7 +4,6 @@ import Header from './header'
import SwrInitor from '@/app/components/swr-initializer'
import { AppContextProvider } from '@/context/app-context'
import GA, { GaType } from '@/app/components/base/ga'
import AmplitudeProvider from '@/app/components/base/amplitude'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import { EventEmitterContextProvider } from '@/context/event-emitter'
import { ProviderContextProvider } from '@/context/provider-context'
@ -14,7 +13,6 @@ const Layout = ({ children }: { children: ReactNode }) => {
return (
<>
<GA gaType={GaType.admin} />
<AmplitudeProvider />
<SwrInitor>
<AppContextProvider>
<EventEmitterContextProvider>

View File

@ -28,7 +28,6 @@ import Input from '@/app/components/base/input'
import { AppModeEnum } from '@/types/app'
import { DSLImportMode } from '@/models/app'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { trackEvent } from '@/app/components/base/amplitude'
type AppsProps = {
onSuccess?: () => void
@ -142,16 +141,6 @@ const Apps = ({
icon_background,
description,
})
// Track app creation from template
trackEvent('create_app_with_template', {
app_mode: mode,
template_id: currApp?.app.id,
template_name: currApp?.app.name,
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
has_description: description,
})
setIsShowCreateModal(false)
Toast.notify({
type: 'success',

View File

@ -30,7 +30,6 @@ import { getRedirection } from '@/utils/app-redirection'
import FullScreenModal from '@/app/components/base/fullscreen-modal'
import useTheme from '@/hooks/use-theme'
import { useDocLink } from '@/context/i18n'
import { trackEvent } from '@/app/components/base/amplitude'
type CreateAppProps = {
onSuccess: () => void
@ -83,14 +82,6 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
mode: appMode,
})
// Track app creation success
trackEvent('create_app', {
app_mode: appMode,
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
description,
})
notify({ type: 'success', message: t('app.newApp.appCreated') })
onSuccess()
onClose()

View File

@ -28,7 +28,6 @@ import { getRedirection } from '@/utils/app-redirection'
import cn from '@/utils/classnames'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { noop } from 'lodash-es'
import { trackEvent } from '@/app/components/base/amplitude'
type CreateFromDSLModalProps = {
show: boolean
@ -113,13 +112,6 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
return
const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
// Track app creation from DSL import
trackEvent('create_app_with_dsl', {
app_mode,
creation_method: currentTab === CreateFromDSLModalTab.FROM_FILE ? 'dsl_file' : 'dsl_url',
has_warnings: status === DSLImportStatus.COMPLETED_WITH_WARNINGS,
})
if (onSuccess)
onSuccess()
if (onClose)

View File

@ -8,7 +8,6 @@ import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import type { QueryParam } from './index'
import Chip from '@/app/components/base/chip'
import Input from '@/app/components/base/input'
import { trackEvent } from '../../base/amplitude/utils'
dayjs.extend(quarterOfYear)
const today = dayjs()
@ -38,9 +37,6 @@ const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps)
value={queryParams.status || 'all'}
onSelect={(item) => {
setQueryParams({ ...queryParams, status: item.value as string })
trackEvent('workflow_log_filter_status_selected', {
workflow_log_filter_status: item.value as string,
})
}}
onClear={() => setQueryParams({ ...queryParams, status: 'all' })}
items={[{ value: 'all', name: 'All' },

View File

@ -282,23 +282,21 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
</>
)}
{
!app.has_draft_trigger && (
(!systemFeatures.webapp_auth.enabled)
? <>
(!systemFeatures.webapp_auth.enabled)
? <>
<Divider className="my-1" />
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
</button>
</>
: !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && (
<>
<Divider className="my-1" />
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
</button>
</>
: !(isGettingUserCanAccessApp || !userCanAccessApp?.result) && (
<>
<Divider className="my-1" />
<button type="button" className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
</button>
</>
)
)
)
}
<Divider className="my-1" />
{

View File

@ -1,47 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import * as amplitude from '@amplitude/analytics-browser'
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
export type IAmplitudeProps = {
apiKey?: string
sessionReplaySampleRate?: number
}
const AmplitudeProvider: FC<IAmplitudeProps> = ({
apiKey = '702e89332ab88a7f14e665f417244e9d',
sessionReplaySampleRate = 1,
}) => {
useEffect(() => {
// // Only enable in non-CE edition
// if (IS_CE_EDITION) {
// console.warn('[Amplitude] Amplitude is disabled in CE edition')
// return
// }
// Initialize Amplitude
amplitude.init(apiKey, {
defaultTracking: {
sessions: true,
pageViews: true,
formInteractions: true,
fileDownloads: true,
},
// Enable debug logs in development environment
logLevel: amplitude.Types.LogLevel.Warn,
})
// Add Session Replay plugin
const sessionReplay = sessionReplayPlugin({
sampleRate: sessionReplaySampleRate,
})
amplitude.add(sessionReplay)
}, [apiKey, sessionReplaySampleRate])
// This is a client component that renders nothing
return null
}
export default React.memo(AmplitudeProvider)

View File

@ -1,2 +0,0 @@
export { default } from './AmplitudeProvider'
export { resetUser, setUserId, setUserProperties, trackEvent } from './utils'

View File

@ -1,37 +0,0 @@
import * as amplitude from '@amplitude/analytics-browser'
/**
* Track custom event
* @param eventName Event name
* @param eventProperties Event properties (optional)
*/
export const trackEvent = (eventName: string, eventProperties?: Record<string, any>) => {
amplitude.track(eventName, eventProperties)
}
/**
* Set user ID
* @param userId User ID
*/
export const setUserId = (userId: string) => {
amplitude.setUserId(userId)
}
/**
* Set user properties
* @param properties User properties
*/
export const setUserProperties = (properties: Record<string, any>) => {
const identifyEvent = new amplitude.Identify()
Object.entries(properties).forEach(([key, value]) => {
identifyEvent.set(key, value)
})
amplitude.identify(identifyEvent)
}
/**
* Reset user (e.g., when user logs out)
*/
export const resetUser = () => {
amplitude.reset()
}

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="apps-2-line">
<path id="Vector" d="M4.66602 7.6665C3.00916 7.6665 1.66602 6.32336 1.66602 4.6665C1.66602 3.00965 3.00916 1.6665 4.66602 1.6665C6.32287 1.6665 7.66602 3.00965 7.66602 4.6665C7.66602 6.32336 6.32287 7.6665 4.66602 7.6665ZM4.66602 14.3332C3.00916 14.3332 1.66602 12.99 1.66602 11.3332C1.66602 9.6763 3.00916 8.33317 4.66602 8.33317C6.32287 8.33317 7.66602 9.6763 7.66602 11.3332C7.66602 12.99 6.32287 14.3332 4.66602 14.3332ZM11.3327 7.6665C9.67582 7.6665 8.33268 6.32336 8.33268 4.6665C8.33268 3.00965 9.67582 1.6665 11.3327 1.6665C12.9895 1.6665 14.3327 3.00965 14.3327 4.6665C14.3327 6.32336 12.9895 7.6665 11.3327 7.6665ZM11.3327 14.3332C9.67582 14.3332 8.33268 12.99 8.33268 11.3332C8.33268 9.6763 9.67582 8.33317 11.3327 8.33317C12.9895 8.33317 14.3327 9.6763 14.3327 11.3332C14.3327 12.99 12.9895 14.3332 11.3327 14.3332ZM4.66602 6.33317C5.58649 6.33317 6.33268 5.58698 6.33268 4.6665C6.33268 3.74603 5.58649 2.99984 4.66602 2.99984C3.74554 2.99984 2.99935 3.74603 2.99935 4.6665C2.99935 5.58698 3.74554 6.33317 4.66602 6.33317ZM4.66602 12.9998C5.58649 12.9998 6.33268 12.2536 6.33268 11.3332C6.33268 10.4127 5.58649 9.6665 4.66602 9.6665C3.74554 9.6665 2.99935 10.4127 2.99935 11.3332C2.99935 12.2536 3.74554 12.9998 4.66602 12.9998ZM11.3327 6.33317C12.2531 6.33317 12.9993 5.58698 12.9993 4.6665C12.9993 3.74603 12.2531 2.99984 11.3327 2.99984C10.4122 2.99984 9.66602 3.74603 9.66602 4.6665C9.66602 5.58698 10.4122 6.33317 11.3327 6.33317ZM11.3327 12.9998C12.2531 12.9998 12.9993 12.2536 12.9993 11.3332C12.9993 10.4127 12.2531 9.6665 11.3327 9.6665C10.4122 9.6665 9.66602 10.4127 9.66602 11.3332C9.66602 12.2536 10.4122 12.9998 11.3327 12.9998Z" fill="#155EEF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.66602 14.3334C3.00916 14.3334 1.66602 12.9903 1.66602 11.3334C1.66602 9.67655 3.00916 8.33342 4.66602 8.33342C6.32287 8.33342 7.66602 9.67655 7.66602 11.3334C7.66602 12.9903 6.32287 14.3334 4.66602 14.3334ZM11.3327 7.66675C9.67582 7.66675 8.33268 6.3236 8.33268 4.66675C8.33268 3.00989 9.67582 1.66675 11.3327 1.66675C12.9895 1.66675 14.3327 3.00989 14.3327 4.66675C14.3327 6.3236 12.9895 7.66675 11.3327 7.66675ZM4.66602 13.0001C5.58649 13.0001 6.33268 12.2539 6.33268 11.3334C6.33268 10.4129 5.58649 9.66675 4.66602 9.66675C3.74554 9.66675 2.99935 10.4129 2.99935 11.3334C2.99935 12.2539 3.74554 13.0001 4.66602 13.0001ZM11.3327 6.33342C12.2531 6.33342 12.9993 5.58722 12.9993 4.66675C12.9993 3.74627 12.2531 3.00008 11.3327 3.00008C10.4122 3.00008 9.66602 3.74627 9.66602 4.66675C9.66602 5.58722 10.4122 6.33342 11.3327 6.33342ZM1.99935 5.33341C1.99935 3.49247 3.49174 2.00008 5.33268 2.00008H7.33268V3.33341H5.33268C4.22812 3.33341 3.33268 4.22885 3.33268 5.33341V7.33342H1.99935V5.33341ZM13.9993 8.66675H12.666V10.6667C12.666 11.7713 11.7706 12.6667 10.666 12.6667H8.66602V14.0001H10.666C12.5069 14.0001 13.9993 12.5077 13.9993 10.6667V8.66675Z" fill="#344054"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 2.66659H3.33333V13.3333H12.6667V5.33325H10V2.66659ZM2 1.99445C2 1.62929 2.29833 1.33325 2.66567 1.33325H10.6667L13.9998 4.66658L14 13.9949C14 14.3659 13.7034 14.6666 13.3377 14.6666H2.66227C2.29651 14.6666 2 14.3631 2 14.0054V1.99445ZM11.7713 7.99992L9.4142 10.3569L8.4714 9.41412L9.8856 7.99992L8.4714 6.58571L9.4142 5.6429L11.7713 7.99992ZM4.22877 7.99992L6.58579 5.6429L7.5286 6.58571L6.11438 7.99992L7.5286 9.41412L6.58579 10.3569L4.22877 7.99992Z" fill="#344054"/>
</svg>

After

Width:  |  Height:  |  Size: 586 B

View File

@ -1,258 +0,0 @@
import type { Meta, StoryObj } from '@storybook/nextjs'
import React from 'react'
declare const require: any
type IconComponent = React.ComponentType<Record<string, unknown>>
type IconEntry = {
name: string
category: string
path: string
Component: IconComponent
}
const iconContext = require.context('./src', true, /\.tsx$/)
const iconEntries: IconEntry[] = iconContext
.keys()
.filter((key: string) => !key.endsWith('.stories.tsx') && !key.endsWith('.spec.tsx'))
.map((key: string) => {
const mod = iconContext(key)
const Component = mod.default as IconComponent | undefined
if (!Component)
return null
const relativePath = key.replace(/^\.\//, '')
const path = `app/components/base/icons/src/${relativePath}`
const parts = relativePath.split('/')
const fileName = parts.pop() || ''
const category = parts.length ? parts.join('/') : '(root)'
const name = Component.displayName || fileName.replace(/\.tsx$/, '')
return {
name,
category,
path,
Component,
}
})
.filter(Boolean) as IconEntry[]
const sortedEntries = [...iconEntries].sort((a, b) => {
if (a.category === b.category)
return a.name.localeCompare(b.name)
return a.category.localeCompare(b.category)
})
const filterEntries = (entries: IconEntry[], query: string) => {
const normalized = query.trim().toLowerCase()
if (!normalized)
return entries
return entries.filter(entry =>
entry.name.toLowerCase().includes(normalized)
|| entry.path.toLowerCase().includes(normalized)
|| entry.category.toLowerCase().includes(normalized),
)
}
const groupByCategory = (entries: IconEntry[]) => entries.reduce((acc, entry) => {
if (!acc[entry.category])
acc[entry.category] = []
acc[entry.category].push(entry)
return acc
}, {} as Record<string, IconEntry[]>)
const containerStyle: React.CSSProperties = {
padding: 24,
display: 'flex',
flexDirection: 'column',
gap: 24,
}
const headerStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: 8,
}
const controlsStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
gap: 12,
flexWrap: 'wrap',
}
const searchInputStyle: React.CSSProperties = {
padding: '8px 12px',
minWidth: 280,
borderRadius: 6,
border: '1px solid #d0d0d5',
}
const toggleButtonStyle: React.CSSProperties = {
padding: '8px 12px',
borderRadius: 6,
border: '1px solid #d0d0d5',
background: '#fff',
cursor: 'pointer',
}
const emptyTextStyle: React.CSSProperties = { color: '#5f5f66' }
const sectionStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: 12,
}
const gridStyle: React.CSSProperties = {
display: 'grid',
gap: 12,
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
}
const cardStyle: React.CSSProperties = {
border: '1px solid #e1e1e8',
borderRadius: 8,
padding: 12,
display: 'flex',
flexDirection: 'column',
gap: 8,
minHeight: 140,
}
const previewBaseStyle: React.CSSProperties = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: 48,
borderRadius: 6,
}
const nameButtonBaseStyle: React.CSSProperties = {
display: 'inline-flex',
padding: 0,
border: 'none',
background: 'transparent',
font: 'inherit',
cursor: 'pointer',
textAlign: 'left',
fontWeight: 600,
}
const PREVIEW_SIZE = 40
const IconGalleryStory = () => {
const [query, setQuery] = React.useState('')
const [copiedPath, setCopiedPath] = React.useState<string | null>(null)
const [previewTheme, setPreviewTheme] = React.useState<'light' | 'dark'>('light')
const filtered = React.useMemo(() => filterEntries(sortedEntries, query), [query])
const grouped = React.useMemo(() => groupByCategory(filtered), [filtered])
const categoryOrder = React.useMemo(
() => Object.keys(grouped).sort((a, b) => a.localeCompare(b)),
[grouped],
)
React.useEffect(() => {
if (!copiedPath)
return undefined
const timerId = window.setTimeout(() => {
setCopiedPath(null)
}, 1200)
return () => window.clearTimeout(timerId)
}, [copiedPath])
const handleCopy = React.useCallback((text: string) => {
navigator.clipboard?.writeText(text)
.then(() => {
setCopiedPath(text)
})
.catch((err) => {
console.error('Failed to copy icon path:', err)
})
}, [])
return (
<div style={containerStyle}>
<header style={headerStyle}>
<h1 style={{ margin: 0 }}>Icon Gallery</h1>
<p style={{ margin: 0, color: '#5f5f66' }}>
Browse all icon components sourced from <code>app/components/base/icons/src</code>. Use the search bar
to filter by name or path.
</p>
<div style={controlsStyle}>
<input
style={searchInputStyle}
placeholder="Search icons"
value={query}
onChange={event => setQuery(event.target.value)}
/>
<span style={{ color: '#5f5f66' }}>{filtered.length} icons</span>
<button
type="button"
onClick={() => setPreviewTheme(prev => (prev === 'light' ? 'dark' : 'light'))}
style={toggleButtonStyle}
>
Toggle {previewTheme === 'light' ? 'dark' : 'light'} preview
</button>
</div>
</header>
{categoryOrder.length === 0 && (
<p style={emptyTextStyle}>No icons match the current filter.</p>
)}
{categoryOrder.map(category => (
<section key={category} style={sectionStyle}>
<h2 style={{ margin: 0, fontSize: 18 }}>{category}</h2>
<div style={gridStyle}>
{grouped[category].map(entry => (
<div key={entry.path} style={cardStyle}>
<div
style={{
...previewBaseStyle,
background: previewTheme === 'dark' ? '#1f2024' : '#fff',
}}
>
<entry.Component style={{ width: PREVIEW_SIZE, height: PREVIEW_SIZE }} />
</div>
<button
type="button"
onClick={() => handleCopy(entry.path)}
style={{
...nameButtonBaseStyle,
color: copiedPath === entry.path ? '#00754a' : '#24262c',
}}
>
{copiedPath === entry.path ? 'Copied!' : entry.name}
</button>
</div>
))}
</div>
</section>
))}
</div>
)
}
const meta: Meta<typeof IconGalleryStory> = {
title: 'Base/Icons/Icon Gallery',
component: IconGalleryStory,
parameters: {
layout: 'fullscreen',
},
}
export default meta
type Story = StoryObj<typeof IconGalleryStory>
export const All: Story = {
render: () => <IconGalleryStory />,
}

View File

@ -0,0 +1,36 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "apps-2-line"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"d": "M4.66602 7.6665C3.00916 7.6665 1.66602 6.32336 1.66602 4.6665C1.66602 3.00965 3.00916 1.6665 4.66602 1.6665C6.32287 1.6665 7.66602 3.00965 7.66602 4.6665C7.66602 6.32336 6.32287 7.6665 4.66602 7.6665ZM4.66602 14.3332C3.00916 14.3332 1.66602 12.99 1.66602 11.3332C1.66602 9.6763 3.00916 8.33317 4.66602 8.33317C6.32287 8.33317 7.66602 9.6763 7.66602 11.3332C7.66602 12.99 6.32287 14.3332 4.66602 14.3332ZM11.3327 7.6665C9.67582 7.6665 8.33268 6.32336 8.33268 4.6665C8.33268 3.00965 9.67582 1.6665 11.3327 1.6665C12.9895 1.6665 14.3327 3.00965 14.3327 4.6665C14.3327 6.32336 12.9895 7.6665 11.3327 7.6665ZM11.3327 14.3332C9.67582 14.3332 8.33268 12.99 8.33268 11.3332C8.33268 9.6763 9.67582 8.33317 11.3327 8.33317C12.9895 8.33317 14.3327 9.6763 14.3327 11.3332C14.3327 12.99 12.9895 14.3332 11.3327 14.3332ZM4.66602 6.33317C5.58649 6.33317 6.33268 5.58698 6.33268 4.6665C6.33268 3.74603 5.58649 2.99984 4.66602 2.99984C3.74554 2.99984 2.99935 3.74603 2.99935 4.6665C2.99935 5.58698 3.74554 6.33317 4.66602 6.33317ZM4.66602 12.9998C5.58649 12.9998 6.33268 12.2536 6.33268 11.3332C6.33268 10.4127 5.58649 9.6665 4.66602 9.6665C3.74554 9.6665 2.99935 10.4127 2.99935 11.3332C2.99935 12.2536 3.74554 12.9998 4.66602 12.9998ZM11.3327 6.33317C12.2531 6.33317 12.9993 5.58698 12.9993 4.6665C12.9993 3.74603 12.2531 2.99984 11.3327 2.99984C10.4122 2.99984 9.66602 3.74603 9.66602 4.6665C9.66602 5.58698 10.4122 6.33317 11.3327 6.33317ZM11.3327 12.9998C12.2531 12.9998 12.9993 12.2536 12.9993 11.3332C12.9993 10.4127 12.2531 9.6665 11.3327 9.6665C10.4122 9.6665 9.66602 10.4127 9.66602 11.3332C9.66602 12.2536 10.4122 12.9998 11.3327 12.9998Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
},
"name": "Apps02"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Apps02.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.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Apps02'
export default Icon

View File

@ -0,0 +1,26 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4.66602 14.3334C3.00916 14.3334 1.66602 12.9903 1.66602 11.3334C1.66602 9.67655 3.00916 8.33342 4.66602 8.33342C6.32287 8.33342 7.66602 9.67655 7.66602 11.3334C7.66602 12.9903 6.32287 14.3334 4.66602 14.3334ZM11.3327 7.66675C9.67582 7.66675 8.33268 6.3236 8.33268 4.66675C8.33268 3.00989 9.67582 1.66675 11.3327 1.66675C12.9895 1.66675 14.3327 3.00989 14.3327 4.66675C14.3327 6.3236 12.9895 7.66675 11.3327 7.66675ZM4.66602 13.0001C5.58649 13.0001 6.33268 12.2539 6.33268 11.3334C6.33268 10.4129 5.58649 9.66675 4.66602 9.66675C3.74554 9.66675 2.99935 10.4129 2.99935 11.3334C2.99935 12.2539 3.74554 13.0001 4.66602 13.0001ZM11.3327 6.33342C12.2531 6.33342 12.9993 5.58722 12.9993 4.66675C12.9993 3.74627 12.2531 3.00008 11.3327 3.00008C10.4122 3.00008 9.66602 3.74627 9.66602 4.66675C9.66602 5.58722 10.4122 6.33342 11.3327 6.33342ZM1.99935 5.33341C1.99935 3.49247 3.49174 2.00008 5.33268 2.00008H7.33268V3.33341H5.33268C4.22812 3.33341 3.33268 4.22885 3.33268 5.33341V7.33342H1.99935V5.33341ZM13.9993 8.66675H12.666V10.6667C12.666 11.7713 11.7706 12.6667 10.666 12.6667H8.66602V14.0001H10.666C12.5069 14.0001 13.9993 12.5077 13.9993 10.6667V8.66675Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "Exchange02"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Exchange02.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.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Exchange02'
export default Icon

View File

@ -0,0 +1,26 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M10 2.66659H3.33333V13.3333H12.6667V5.33325H10V2.66659ZM2 1.99445C2 1.62929 2.29833 1.33325 2.66567 1.33325H10.6667L13.9998 4.66658L14 13.9949C14 14.3659 13.7034 14.6666 13.3377 14.6666H2.66227C2.29651 14.6666 2 14.3631 2 14.0054V1.99445ZM11.7713 7.99992L9.4142 10.3569L8.4714 9.41412L9.8856 7.99992L8.4714 6.58571L9.4142 5.6429L11.7713 7.99992ZM4.22877 7.99992L6.58579 5.6429L7.5286 6.58571L6.11438 7.99992L7.5286 9.41412L6.58579 10.3569L4.22877 7.99992Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "FileCode"
}

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './FileCode.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.RefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'FileCode'
export default Icon

View File

@ -1,7 +1,10 @@
export { default as Apps02 } from './Apps02'
export { default as BubbleX } from './BubbleX'
export { default as Colors } from './Colors'
export { default as DragHandle } from './DragHandle'
export { default as Env } from './Env'
export { default as Exchange02 } from './Exchange02'
export { default as FileCode } from './FileCode'
export { default as GlobalVariable } from './GlobalVariable'
export { default as Icon3Dots } from './Icon3Dots'
export { default as LongArrowLeft } from './LongArrowLeft'

View File

@ -121,7 +121,7 @@ const RegenerationModal: FC<IRegenerationModalProps> = ({
})
return (
<Modal isShow={isShow} onClose={noop} className='!max-w-[480px] !rounded-2xl' wrapperClassName='!z-[10000]'>
<Modal isShow={isShow} onClose={noop} className='!max-w-[480px] !rounded-2xl'>
{!loading && !updateSucceeded && <DefaultContent onCancel={onCancel} onConfirm={onConfirm} />}
{loading && !updateSucceeded && <RegeneratingContent />}
{!loading && updateSucceeded && <RegenerationCompletedContent onClose={onClose} />}

View File

@ -124,7 +124,6 @@ const Completed: FC<ICompletedProps> = ({
const [limit, setLimit] = useState(DEFAULT_LIMIT)
const [fullScreen, setFullScreen] = useState(false)
const [showNewChildSegmentModal, setShowNewChildSegmentModal] = useState(false)
const [isRegenerationModalOpen, setIsRegenerationModalOpen] = useState(false)
const segmentListRef = useRef<HTMLDivElement>(null)
const childSegmentListRef = useRef<HTMLDivElement>(null)
@ -670,7 +669,6 @@ const Completed: FC<ICompletedProps> = ({
onClose={onCloseSegmentDetail}
showOverlay={false}
needCheckChunks
modal={isRegenerationModalOpen}
>
<SegmentDetail
key={currSegment.segInfo?.id}
@ -679,7 +677,6 @@ const Completed: FC<ICompletedProps> = ({
isEditMode={currSegment.isEditMode}
onUpdate={handleUpdateSegment}
onCancel={onCloseSegmentDetail}
onModalStateChange={setIsRegenerationModalOpen}
/>
</FullScreenDrawer>
{/* Create New Segment */}

View File

@ -27,7 +27,6 @@ type ISegmentDetailProps = {
onCancel: () => void
isEditMode?: boolean
docForm: ChunkingMode
onModalStateChange?: (isOpen: boolean) => void
}
/**
@ -39,7 +38,6 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
onCancel,
isEditMode,
docForm,
onModalStateChange,
}) => {
const { t } = useTranslation()
const [question, setQuestion] = useState(isEditMode ? segInfo?.content || '' : segInfo?.sign_content || '')
@ -70,19 +68,11 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
const handleRegeneration = useCallback(() => {
setShowRegenerationModal(true)
onModalStateChange?.(true)
}, [onModalStateChange])
}, [])
const onCancelRegeneration = useCallback(() => {
setShowRegenerationModal(false)
onModalStateChange?.(false)
}, [onModalStateChange])
const onCloseAfterRegeneration = useCallback(() => {
setShowRegenerationModal(false)
onModalStateChange?.(false)
onCancel() // Close the edit drawer
}, [onCancel, onModalStateChange])
}, [])
const onConfirmRegeneration = useCallback(() => {
onUpdate(segInfo?.id || '', question, answer, keywords, true)
@ -171,7 +161,7 @@ const SegmentDetail: FC<ISegmentDetailProps> = ({
isShow={showRegenerationModal}
onConfirm={onConfirmRegeneration}
onCancel={onCancelRegeneration}
onClose={onCloseAfterRegeneration}
onClose={onCancelRegeneration}
/>
)
}

View File

@ -17,9 +17,8 @@ import type { InvitationResult } from '@/models/common'
import I18n from '@/context/i18n'
import 'react-multi-email/dist/style.css'
import { noop } from 'lodash-es'
import { useProviderContextSelector } from '@/context/provider-context'
import { useBoolean } from 'ahooks'
import { useProviderContextSelector } from '@/context/provider-context'
type IInviteModalProps = {
isEmailSetup: boolean
onCancel: () => void
@ -50,15 +49,9 @@ const InviteModal = ({
const { locale } = useContext(I18n)
const [role, setRole] = useState<string>('normal')
const [isSubmitting, {
setTrue: setIsSubmitting,
setFalse: setIsSubmitted,
}] = useBoolean(false)
const handleSend = useCallback(async () => {
if (isLimitExceeded || isSubmitting)
if (isLimitExceeded)
return
setIsSubmitting()
if (emails.map((email: string) => emailRegex.test(email)).every(Boolean)) {
try {
const { result, invitation_results } = await inviteMember({
@ -77,8 +70,7 @@ const InviteModal = ({
else {
notify({ type: 'error', message: t('common.members.emailInvalid') })
}
setIsSubmitted()
}, [isLimitExceeded, emails, role, locale, onCancel, onSend, notify, t, isSubmitting])
}, [isLimitExceeded, emails, role, locale, onCancel, onSend, notify, t])
return (
<div className={cn(s.wrap)}>
@ -141,7 +133,7 @@ const InviteModal = ({
tabIndex={0}
className='w-full'
onClick={handleSend}
disabled={!emails.length || isLimitExceeded || isSubmitting}
disabled={!emails.length || isLimitExceeded}
variant='primary'
>
{t('common.members.sendInvite')}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,80 @@
'use client'
import { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useMount } from 'ahooks'
import cn from '@/utils/classnames'
import { Apps02 } from '@/app/components/base/icons/src/vender/line/others'
import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n-config/language'
import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
import { fetchLabelList } from '@/service/tools'
import { renderI18nObject } from '@/i18n-config'
type Props = {
value: string
onSelect: (type: string) => void
}
const Icon = ({ svgString, active }: { svgString: string; active: boolean }) => {
const svgRef = useRef<SVGSVGElement | null>(null)
const SVGParser = (svg: string) => {
if (!svg)
return null
const parser = new DOMParser()
const doc = parser.parseFromString(svg, 'image/svg+xml')
return doc.documentElement
}
useMount(() => {
const svgElement = SVGParser(svgString)
if (svgRef.current && svgElement)
svgRef.current.appendChild(svgElement)
})
return <svg className={cn('h-4 w-4 text-gray-700', active && '!text-primary-600')} ref={svgRef} />
}
const Category = ({
value,
onSelect,
}: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const language = getLanguage(locale)
const labelList = useLabelStore(s => s.labelList)
const setLabelList = useLabelStore(s => s.setLabelList)
useMount(() => {
fetchLabelList().then((res) => {
setLabelList(res)
})
})
return (
<div className='mb-3'>
<div className='px-3 py-0.5 text-xs font-medium leading-[18px] text-gray-500'>{t('tools.addToolModal.category').toLocaleUpperCase()}</div>
<div className={cn('mb-0.5 flex cursor-pointer items-center rounded-lg p-1 pl-3 text-sm leading-5 text-gray-700 hover:bg-white', value === '' && '!bg-white font-medium !text-primary-600')} onClick={() => onSelect('')}>
<Apps02 className='mr-2 h-4 w-4 shrink-0' />
{t('tools.type.all')}
</div>
{labelList.map((label) => {
const labelText = typeof label.label === 'string'
? label.label
: (label.label ? renderI18nObject(label.label, language) : '')
return (
<div
key={label.name}
title={labelText}
className={cn('mb-0.5 flex cursor-pointer items-center overflow-hidden truncate rounded-lg p-1 pl-3 text-sm leading-5 text-gray-700 hover:bg-white', value === label.name && '!bg-white font-medium !text-primary-600')}
onClick={() => onSelect(label.name)}
>
<div className='mr-2 h-4 w-4 shrink-0'>
<Icon active={value === label.name} svgString={label.icon || ''} />
</div>
{labelText}
</div>
)
})}
</div>
)
}
export default Category

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,258 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { produce } from 'immer'
import {
RiAddLine,
RiCloseLine,
} from '@remixicon/react'
import { useMount } from 'ahooks'
import type { Collection, CustomCollectionBackend, Tool } from '../types'
import type { CollectionType } from '../types'
import Type from './type'
import Category from './category'
import Tools from './tools'
import cn from '@/utils/classnames'
import { basePath } from '@/utils/var'
import I18n from '@/context/i18n'
import Drawer from '@/app/components/base/drawer'
import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading'
import Input from '@/app/components/base/input'
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import {
createCustomCollection,
fetchAllBuiltInTools,
fetchAllCustomTools,
fetchAllWorkflowTools,
removeBuiltInToolCredential,
updateBuiltInToolCredential,
} from '@/service/tools'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import Toast from '@/app/components/base/toast'
import ConfigContext from '@/context/debug-configuration'
import type { ModelConfig } from '@/models/debug'
type Props = {
onHide: () => void
}
// Add and Edit
const AddToolModal: FC<Props> = ({
onHide,
}) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const [currentType, setCurrentType] = useState('builtin')
const [currentCategory, setCurrentCategory] = useState('')
const [keywords, setKeywords] = useState<string>('')
const handleKeywordsChange = (value: string) => {
setKeywords(value)
}
const isMatchingKeywords = (text: string, keywords: string) => {
return text.toLowerCase().includes(keywords.toLowerCase())
}
const [toolList, setToolList] = useState<ToolWithProvider[]>([])
const [listLoading, setListLoading] = useState(true)
const getAllTools = async () => {
setListLoading(true)
const buildInTools = await fetchAllBuiltInTools()
if (basePath) {
buildInTools.forEach((item) => {
if (typeof item.icon == 'string' && !item.icon.includes(basePath))
item.icon = `${basePath}${item.icon}`
})
}
const customTools = await fetchAllCustomTools()
const workflowTools = await fetchAllWorkflowTools()
const mergedToolList = [
...buildInTools,
...customTools,
...workflowTools.filter((toolWithProvider) => {
return !toolWithProvider.tools.some((tool) => {
return !!tool.parameters.find(item => item.name === '__image')
})
}),
]
setToolList(mergedToolList)
setListLoading(false)
}
const filteredList = useMemo(() => {
return toolList.filter((toolWithProvider) => {
if (currentType === 'all')
return true
else
return toolWithProvider.type === currentType
}).filter((toolWithProvider) => {
if (!currentCategory)
return true
else
return toolWithProvider.labels.includes(currentCategory)
}).filter((toolWithProvider) => {
return (
isMatchingKeywords(toolWithProvider.name, keywords)
|| toolWithProvider.tools.some((tool) => {
return Object.values(tool.label).some((label) => {
return isMatchingKeywords(label, keywords)
})
})
)
})
}, [currentType, currentCategory, toolList, keywords])
const {
modelConfig,
setModelConfig,
} = useContext(ConfigContext)
const [isShowEditCollectionToolModal, setIsShowEditCustomCollectionModal] = useState(false)
const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => {
await createCustomCollection(data)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setIsShowEditCustomCollectionModal(false)
getAllTools()
}
const [showSettingAuth, setShowSettingAuth] = useState(false)
const [collection, setCollection] = useState<Collection>()
const toolSelectHandle = (collection: Collection, tool: Tool) => {
const parameters: Record<string, string> = {}
if (tool.parameters) {
tool.parameters.forEach((item) => {
parameters[item.name] = ''
})
}
const nexModelConfig = produce(modelConfig, (draft: ModelConfig) => {
draft.agentConfig.tools.push({
provider_id: collection.id || collection.name,
provider_type: collection.type as CollectionType,
provider_name: collection.name,
tool_name: tool.name,
tool_label: tool.label[locale] || tool.label[locale.replaceAll('-', '_')],
tool_parameters: parameters,
enabled: true,
})
})
setModelConfig(nexModelConfig)
}
const authSelectHandle = (provider: Collection) => {
setCollection(provider)
setShowSettingAuth(true)
}
const updateBuiltinAuth = async (value: Record<string, any>) => {
if (!collection)
return
await updateBuiltInToolCredential(collection.name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await getAllTools()
setShowSettingAuth(false)
}
const removeBuiltinAuth = async () => {
if (!collection)
return
await removeBuiltInToolCredential(collection.name)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
await getAllTools()
setShowSettingAuth(false)
}
useMount(() => {
getAllTools()
})
return (
<>
<Drawer
isOpen
mask
clickOutsideNotOpen
onClose={onHide}
footer={null}
panelClassName={cn('mx-2 mb-3 mt-16 rounded-xl !p-0 sm:mr-2', 'mt-2 !w-[640px]', '!max-w-[640px]')}
>
<div
className='flex w-full rounded-xl border-[0.5px] border-gray-200 bg-white shadow-xl'
style={{
height: 'calc(100vh - 16px)',
}}
>
<div className='relative w-[200px] shrink-0 overflow-y-auto rounded-l-xl border-r-[0.5px] border-black/2 bg-gray-100 pb-3'>
<div className='sticky left-0 right-0 top-0'>
<div className='text-md sticky left-0 right-0 top-0 px-5 py-3 font-semibold text-gray-900'>{t('tools.addTool')}</div>
<div className='px-3 pb-4 pt-2'>
<Button variant='primary' className='w-[176px]' onClick={() => setIsShowEditCustomCollectionModal(true)}>
<RiAddLine className='mr-1 h-4 w-4' />
{t('tools.createCustomTool')}
</Button>
</div>
</div>
<div className='px-2 py-1'>
<Type value={currentType} onSelect={setCurrentType} />
<Category value={currentCategory} onSelect={setCurrentCategory} />
</div>
</div>
<div className='relative grow overflow-y-auto rounded-r-xl bg-white'>
<div className='sticky left-0 right-0 top-0 z-10 flex items-center gap-1 bg-white p-2'>
<div className='grow'>
<Input
showLeftIcon
showClearIcon
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
</div>
<div className='ml-2 mr-1 h-4 w-[1px] bg-gray-200'></div>
<div className='cursor-pointer p-2' onClick={onHide}>
<RiCloseLine className='h-4 w-4 text-gray-500' />
</div>
</div>
{listLoading && (
<div className='flex h-[200px] items-center justify-center bg-white'>
<Loading />
</div>
)}
{!listLoading && (
<Tools
showWorkflowEmpty={currentType === 'workflow'}
tools={filteredList}
addedTools={(modelConfig?.agentConfig?.tools as any) || []}
onSelect={toolSelectHandle}
onAuthSetup={authSelectHandle}
/>
)}
</div>
</div>
</Drawer>
{isShowEditCollectionToolModal && (
<EditCustomToolModal
positionLeft
payload={null}
onHide={() => setIsShowEditCustomCollectionModal(false)}
onAdd={doCreateCustomToolCollection}
/>
)}
{showSettingAuth && collection && (
<ConfigCredential
collection={collection}
onCancel={() => setShowSettingAuth(false)}
onSaved={updateBuiltinAuth}
onRemove={removeBuiltinAuth}
/>
)}
</>
)
}
export default React.memo(AddToolModal)

View File

@ -0,0 +1,158 @@
import {
memo,
useCallback,
} from 'react'
import { basePath } from '@/utils/var'
import { useTranslation } from 'react-i18next'
import {
RiAddLine,
} from '@remixicon/react'
import cn from '@/utils/classnames'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
import { Tag01 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import BlockIcon from '@/app/components/workflow/block-icon'
import Tooltip from '@/app/components/base/tooltip'
import Button from '@/app/components/base/button'
import { useGetLanguage } from '@/context/i18n'
import { useStore as useLabelStore } from '@/app/components/tools/labels/store'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import type { Tool } from '@/app/components/tools/types'
import { CollectionType } from '@/app/components/tools/types'
import type { AgentTool } from '@/types/app'
import { MAX_TOOLS_NUM } from '@/config'
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { renderI18nObject } from '@/i18n-config'
const resolveI18nText = (value: TypeWithI18N | string | undefined, language: string): string => {
if (!value)
return ''
return typeof value === 'string' ? value : renderI18nObject(value, language)
}
type ToolsProps = {
showWorkflowEmpty: boolean
tools: ToolWithProvider[]
addedTools: AgentTool[]
onSelect: (provider: ToolWithProvider, tool: Tool) => void
onAuthSetup: (provider: ToolWithProvider) => void
}
const Blocks = ({
showWorkflowEmpty,
tools,
addedTools,
onSelect,
onAuthSetup,
}: ToolsProps) => {
const { t } = useTranslation()
const language = useGetLanguage()
const labelList = useLabelStore(s => s.labelList)
const addable = addedTools.length < MAX_TOOLS_NUM
const renderGroup = useCallback((toolWithProvider: ToolWithProvider) => {
const list = toolWithProvider.tools
const needAuth = toolWithProvider.allow_delete && !toolWithProvider.is_team_authorization && toolWithProvider.type === CollectionType.builtIn
return (
<div
key={toolWithProvider.id}
className='group mb-1 last-of-type:mb-0'
>
<div className='flex h-[22px] w-full items-center justify-between pl-3 pr-1 text-xs font-medium text-gray-500'>
{resolveI18nText(toolWithProvider.label, language)}
<a className='hidden cursor-pointer items-center group-hover:flex' href={`${basePath}/tools?category=${toolWithProvider.type}`} target='_blank'>{t('tools.addToolModal.manageInTools')}<ArrowUpRight className='ml-0.5 h-3 w-3' /></a>
</div>
{list.map((tool) => {
const labelContent = (() => {
if (!tool.labels)
return ''
return tool.labels.map((name) => {
const label = labelList.find(item => item.name === name)
return resolveI18nText(label?.label, language)
}).filter(Boolean).join(', ')
})()
const added = !!addedTools?.find(v => v.provider_id === toolWithProvider.id && v.provider_type === toolWithProvider.type && v.tool_name === tool.name)
return (
<Tooltip
key={tool.name}
position='bottom'
popupClassName='!p-0 !px-3 !py-2.5 !w-[210px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !bg-transparent !rounded-xl !shadow-lg translate-x-[108px]'
popupContent={(
<div>
<BlockIcon
size='md'
className='mb-2'
type={BlockEnum.Tool}
toolIcon={toolWithProvider.icon}
/>
<div className='mb-1 text-sm leading-5 text-gray-900'>{resolveI18nText(tool.label, language)}</div>
<div className='text-xs leading-[18px] text-gray-700'>{resolveI18nText(tool.description, language)}</div>
{tool.labels?.length > 0 && (
<div className='mt-1 flex shrink-0 items-center'>
<div className='relative flex w-full items-center gap-1 rounded-md py-1 text-gray-500' title={labelContent}>
<Tag01 className='h-3 w-3 shrink-0 text-gray-500' />
<div className='grow truncate text-start text-xs font-normal leading-[18px]'>{labelContent}</div>
</div>
</div>
)}
</div>
)}
>
<div className='group/item flex h-8 w-full cursor-pointer items-center rounded-lg pl-3 pr-1 hover:bg-gray-50'>
<BlockIcon
className={cn('mr-2 shrink-0', needAuth && 'opacity-30')}
type={BlockEnum.Tool}
toolIcon={toolWithProvider.icon}
/>
<div className={cn('grow truncate text-sm text-gray-900', needAuth && 'opacity-30')}>{resolveI18nText(tool.label, language)}</div>
{!needAuth && added && (
<div className='flex items-center gap-1 rounded-[6px] border border-gray-100 bg-white px-2 py-[3px] text-xs font-medium leading-[18px] text-gray-300'>
<Check className='h-3 w-3' />
{t('tools.addToolModal.added').toLocaleUpperCase()}
</div>
)}
{!needAuth && !added && addable && (
<Button
variant='secondary-accent'
size='small'
className={cn('hidden shrink-0 items-center group-hover/item:flex')}
onClick={() => onSelect(toolWithProvider, tool)}
>
<RiAddLine className='h-3 w-3' />
{t('tools.addToolModal.add').toLocaleUpperCase()}
</Button>
)}
{needAuth && (
<Button
variant='secondary-accent'
size='small'
className={cn('hidden shrink-0 group-hover/item:flex')}
onClick={() => onAuthSetup(toolWithProvider)}
>{t('tools.auth.setup')}</Button>
)}
</div>
</Tooltip>
)
})}
</div>
)
}, [addable, language, t, labelList, addedTools, onAuthSetup, onSelect])
return (
<div className='max-w-[440px] p-1 pb-6'>
{!tools.length && !showWorkflowEmpty && (
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
)}
{!tools.length && showWorkflowEmpty && (
<div className='pt-[280px]'>
<Empty />
</div>
)}
{!!tools.length && tools.map(renderGroup)}
</div>
)
}
export default memo(Blocks)

View File

@ -0,0 +1,34 @@
'use client'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import { Exchange02, FileCode } from '@/app/components/base/icons/src/vender/line/others'
type Props = {
value: string
onSelect: (type: string) => void
}
const Types = ({
value,
onSelect,
}: Props) => {
const { t } = useTranslation()
return (
<div className='mb-3'>
<div className={cn('mb-0.5 flex cursor-pointer items-center rounded-lg p-1 pl-3 text-sm leading-5 hover:bg-white', value === 'builtin' && '!bg-white font-medium')} onClick={() => onSelect('builtin')}>
<div className="mr-2 h-4 w-4 shrink-0 bg-[url('~@/app/components/tools/add-tool-modal/D.png')] bg-cover bg-no-repeat" />
<span className={cn('text-gray-700', value === 'builtin' && '!text-primary-600')}>{t('tools.type.builtIn')}</span>
</div>
<div className={cn('mb-0.5 flex cursor-pointer items-center rounded-lg p-1 pl-3 text-sm leading-5 text-gray-700 hover:bg-white', value === 'api' && '!bg-white font-medium !text-primary-600')} onClick={() => onSelect('api')}>
<FileCode className='mr-2 h-4 w-4 shrink-0' />
{t('tools.type.custom')}
</div>
<div className={cn('mb-0.5 flex cursor-pointer items-center rounded-lg p-1 pl-3 text-sm leading-5 text-gray-700 hover:bg-white', value === 'workflow' && '!bg-white font-medium !text-primary-600')} onClick={() => onSelect('workflow')}>
<Exchange02 className='mr-2 h-4 w-4 shrink-0' />
{t('tools.type.workflow')}
</div>
</div>
)
}
export default Types

View File

@ -11,7 +11,7 @@ import Input from '@/app/components/base/input'
import ProviderDetail from '@/app/components/tools/provider/detail'
import Empty from '@/app/components/plugins/marketplace/empty'
import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
import WorkflowToolEmpty from '@/app/components/tools/provider/empty'
import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'

View File

@ -17,17 +17,9 @@ export const useAvailableNodesMetaData = () => {
const isChatMode = useIsChatMode()
const docLink = useDocLink()
const startNodeMetaData = useMemo(() => ({
...StartDefault,
metaData: {
...StartDefault.metaData,
isUndeletable: isChatMode, // start node is undeletable in chat mode, @use-nodes-interactions: handleNodeDelete function
},
}), [isChatMode])
const mergedNodesMetaData = useMemo(() => [
...WORKFLOW_COMMON_NODES,
startNodeMetaData,
StartDefault,
...(
isChatMode
? [AnswerDefault]
@ -38,7 +30,7 @@ export const useAvailableNodesMetaData = () => {
TriggerPluginDefault,
]
),
], [isChatMode, startNodeMetaData])
], [isChatMode])
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
const { metaData } = node

View File

@ -4,7 +4,7 @@ import IndexBar, { groupItems } from './index-bar'
import type { ToolDefaultValue, ToolValue } from './types'
import type { ToolTypeEnum } from './types'
import { ViewType } from './view-type-select'
import Empty from '@/app/components/tools/provider/empty'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import { useGetLanguage } from '@/context/i18n'
import ToolListTreeView from './tool/tool-list-tree-view/list'
import ToolListFlatView from './tool/tool-list-flat-view/list'

View File

@ -249,8 +249,6 @@ export const useChecklistBeforePublish = () => {
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const appMode = useAppStore.getState().appDetail?.mode
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
let checkData = data
@ -368,22 +366,17 @@ export const useChecklistBeforePublish = () => {
}
}
const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
const isUnconnected = !validNodes.find(n => n.id === node.id)
if (isUnconnected && !canSkipConnectionCheck) {
if (!validNodes.find(n => n.id === node.id)) {
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnectTip')}` })
return false
}
}
if (shouldCheckStartNode) {
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
if (startNodesFiltered.length === 0) {
notify({ type: 'error', message: t('workflow.common.needStartNode') })
return false
}
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
if (startNodesFiltered.length === 0) {
notify({ type: 'error', message: t('workflow.common.needStartNode') })
return false
}
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
@ -398,7 +391,7 @@ export const useChecklistBeforePublish = () => {
}
return true
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode])
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools])
return {
handleCheckBeforePublish,

View File

@ -44,17 +44,17 @@ export const useShortcuts = (): void => {
fitView,
} = useReactFlow()
// Zoom out to a minimum of 0.25 for shortcut
// Zoom out to a minimum of 0.5 for shortcut
const constrainedZoomOut = () => {
const currentZoom = getZoom()
const newZoom = Math.max(currentZoom - 0.1, 0.25)
const newZoom = Math.max(currentZoom - 0.1, 0.5)
zoomTo(newZoom)
}
// Zoom in to a maximum of 2 for shortcut
// Zoom in to a maximum of 1 for shortcut
const constrainedZoomIn = () => {
const currentZoom = getZoom()
const newZoom = Math.min(currentZoom + 0.1, 2)
const newZoom = Math.min(currentZoom + 0.1, 1)
zoomTo(newZoom)
}

View File

@ -76,11 +76,9 @@ const RetryOnPanel = ({
/>
<Input
type='number'
wrapperClassName='w-[100px]'
wrapperClassName='w-[80px]'
value={retry_config?.max_retries || 3}
onChange={e =>
handleMaxRetriesChange(Number.parseInt(e.currentTarget.value, 10) || 3)
}
onChange={e => handleMaxRetriesChange(e.target.value as any)}
min={1}
max={10}
unit={t('workflow.nodes.common.retry.times') || ''}
@ -98,11 +96,9 @@ const RetryOnPanel = ({
/>
<Input
type='number'
wrapperClassName='w-[100px]'
wrapperClassName='w-[80px]'
value={retry_config?.retry_interval || 1000}
onChange={e =>
handleRetryIntervalChange(Number.parseInt(e.currentTarget.value, 10) || 1000)
}
onChange={e => handleRetryIntervalChange(e.target.value as any)}
min={100}
max={5000}
unit={t('workflow.nodes.common.retry.ms') || ''}

View File

@ -65,7 +65,7 @@ const AgentNode: FC<NodeProps<AgentNodeType>> = (props) => {
})
return tools
}, [currentStrategy?.parameters, inputs.agent_parameters])
return <div className='mb-1 space-y-1 px-3'>
return <div className='mb-1 space-y-1 px-3 py-1'>
{inputs.agent_strategy_name
? <SettingItem
label={t('workflow.nodes.agent.strategy.shortLabel')}

View File

@ -25,7 +25,7 @@ const NodeComponent: FC<NodeProps<DocExtractorNodeType>> = ({
const isSystem = isSystemVar(variable)
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
return (
<div className='relative mb-1 px-3 py-1'>
<div className='relative px-3'>
<div className='system-2xs-medium-uppercase mb-1 text-text-tertiary'>{t(`${i18nPrefix}.inputVar`)}</div>
<VariableLabelInNode
variables={variable}

View File

@ -11,7 +11,6 @@ import Toast from '@/app/components/base/toast'
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
import I18NContext from '@/context/i18n'
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
import { trackEvent } from '@/app/components/base/amplitude'
export default function CheckCode() {
const { t, i18n } = useTranslation()
@ -44,13 +43,6 @@ export default function CheckCode() {
setIsLoading(true)
const ret = await emailLoginWithCode({ email, code, token, language })
if (ret.result === 'success') {
// Track login success event
trackEvent('user_login_success', {
method: 'email_code',
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
is_invite: !!invite_token,
})
if (invite_token) {
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}

View File

@ -12,7 +12,6 @@ import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
import type { ResponseError } from '@/service/fetch'
import { trackEvent } from '@/app/components/base/amplitude'
type MailAndPasswordAuthProps = {
isInvite: boolean
@ -64,13 +63,6 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
body: loginData,
})
if (res.result === 'success') {
// Track login success event
trackEvent('user_login_success', {
method: 'email_password',
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
is_invite: isInvite,
})
if (isInvite) {
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
}

View File

@ -42,6 +42,7 @@ export default function CheckCode() {
}
setIsLoading(true)
const res = await verifyCode({ email, code, token })
console.log(res)
if ((res as MailValidityResponse).is_valid) {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent((res as MailValidityResponse).token))

View File

@ -9,7 +9,6 @@ import Input from '@/app/components/base/input'
import { validPassword } from '@/config'
import type { MailRegisterResponse } from '@/service/use-common'
import { useMailRegister } from '@/service/use-common'
import { trackEvent } from '@/app/components/base/amplitude'
const ChangePasswordForm = () => {
const { t } = useTranslation()
@ -55,12 +54,6 @@ const ChangePasswordForm = () => {
})
const { result } = res as MailRegisterResponse
if (result === 'success') {
// Track registration success event
trackEvent('user_registration_success', {
method: 'email',
time: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
})
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),

View File

@ -10,7 +10,6 @@ import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { noop } from 'lodash-es'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import { ZENDESK_FIELD_IDS } from '@/config'
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
import { useGlobalPublicStore } from './global-public-context'
export type AppContextValue = {
@ -160,33 +159,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
}, [currentWorkspace?.id])
// #endregion Zendesk conversation fields
// #region Amplitude user tracking
useEffect(() => {
// Report user info to Amplitude when loaded
if (userProfile?.id) {
setUserId(userProfile.email)
setUserProperties({
email: userProfile.email,
name: userProfile.name,
has_password: userProfile.is_password_set,
})
}
}, [userProfile?.id, userProfile?.email, userProfile?.name, userProfile?.is_password_set])
useEffect(() => {
// Report workspace info to Amplitude when loaded
if (currentWorkspace?.id && userProfile?.id) {
setUserProperties({
workspace_id: currentWorkspace.id,
workspace_name: currentWorkspace.name,
workspace_plan: currentWorkspace.plan,
workspace_status: currentWorkspace.status,
workspace_role: currentWorkspace.role,
})
}
}, [currentWorkspace?.id, currentWorkspace?.name, currentWorkspace?.plan, currentWorkspace?.status, currentWorkspace?.role, userProfile?.id])
// #endregion Amplitude user tracking
return (
<AppContext.Provider value={{
userProfile,

View File

@ -2,6 +2,7 @@ const translation = {
title: 'Werkzeuge',
createCustomTool: 'Eigenes Werkzeug erstellen',
type: {
all: 'Alle',
builtIn: 'Integriert',
custom: 'Benutzerdefiniert',
workflow: 'Arbeitsablauf',
@ -19,6 +20,7 @@ const translation = {
setupModalTitleDescription: 'Nach der Konfiguration der Anmeldeinformationen können alle Mitglieder im Arbeitsbereich dieses Werkzeug beim Orchestrieren von Anwendungen nutzen.',
},
includeToolNum: '{{num}} Werkzeuge inkludiert',
addTool: 'Werkzeug hinzufügen',
createTool: {
title: 'Eigenes Werkzeug erstellen',
editAction: 'Konfigurieren',
@ -141,7 +143,9 @@ const translation = {
addToolModal: {
type: 'Art',
category: 'Kategorie',
add: 'hinzufügen',
added: 'zugefügt',
manageInTools: 'Verwalten in Tools',
custom: {
title: 'Kein benutzerdefiniertes Werkzeug verfügbar',
tip: 'Benutzerdefiniertes Werkzeug erstellen',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'Create Custom Tool',
customToolTip: 'Learn more about Dify custom tools',
type: {
all: 'All',
builtIn: 'Tools',
custom: 'Custom',
workflow: 'Workflow',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: 'After configuring credentials, all members within the workspace can use this tool when orchestrating applications.',
},
includeToolNum: '{{num}} {{action}} included',
addTool: 'Add Tool',
addToolModal: {
type: 'type',
category: 'category',
add: 'add',
added: 'added',
manageInTools: 'Manage in Tools',
custom: {
title: 'No custom tool available',
tip: 'Create a custom tool',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'Crear Herramienta Personalizada',
customToolTip: 'Aprende más sobre las herramientas personalizadas de Dify',
type: {
all: 'Todas',
builtIn: 'Incorporadas',
custom: 'Personalizadas',
workflow: 'Flujo de Trabajo',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: 'Después de configurar las credenciales, todos los miembros dentro del espacio de trabajo pueden usar esta herramienta al orquestar aplicaciones.',
},
includeToolNum: '{{num}} herramientas incluidas',
addTool: 'Agregar Herramienta',
addToolModal: {
type: 'tipo',
category: 'categoría',
add: 'agregar',
added: 'agregada',
manageInTools: 'Administrar en Herramientas',
custom: {
title: 'No hay herramienta personalizada disponible',
tip: 'Crear una herramienta personalizada',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'ایجاد ابزار سفارشی',
customToolTip: 'بیشتر در مورد ابزارهای سفارشی Dify بیاموزید',
type: {
all: 'همه',
builtIn: 'سفارشی شده',
custom: 'سفارشی',
workflow: 'جریان کار',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: 'پس از پیکربندی اعتبارنامه‌ها، همه اعضای موجود در فضای کاری می‌توانند از این ابزار هنگام هماهنگی برنامه‌ها استفاده کنند.',
},
includeToolNum: '{{num}} ابزار شامل شد',
addTool: 'افزودن ابزار',
addToolModal: {
type: 'نوع',
category: 'دسته‌بندی',
add: 'افزودن',
added: 'افزوده شد',
manageInTools: 'مدیریت در ابزارها',
custom: {
title: 'هیچ ابزار سفارشی موجود نیست',
tip: 'یک ابزار سفارشی ایجاد کنید',

View File

@ -2,6 +2,7 @@ const translation = {
title: 'Outils',
createCustomTool: 'Créer un Outil Personnalisé',
type: {
all: 'Tout',
builtIn: 'Intégré',
custom: 'Personnalisé',
workflow: 'Flux de travail',
@ -19,6 +20,7 @@ const translation = {
setupModalTitleDescription: 'Après avoir configuré les identifiants, tous les membres de l\'espace de travail peuvent utiliser cet outil lors de l\'orchestration des applications.',
},
includeToolNum: '{{num}} outils inclus',
addTool: 'Ajouter un outil',
createTool: {
title: 'Créer un Outil Personnalisé',
editAction: 'Configurer',
@ -141,7 +143,9 @@ const translation = {
addToolModal: {
type: 'type',
added: 'supplémentaire',
add: 'ajouter',
category: 'catégorie',
manageInTools: 'Gérer dans Outils',
custom: {
title: 'Aucun outil personnalisé disponible',
tip: 'Créer un outil personnalisé',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'कस्टम उपकरण बनाएं',
customToolTip: 'Dify कस्टम उपकरणों के बारे में और जानें',
type: {
all: 'सभी',
builtIn: 'निर्मित',
custom: 'कस्टम',
workflow: 'कार्यप्रवाह',
@ -21,10 +22,13 @@ const translation = {
'प्रमाणिकरण कॉन्फ़िगर करने के बाद, कार्यस्थान के सभी सदस्य इस उपकरण का उपयोग कर सकेंगे।',
},
includeToolNum: '{{num}} उपकरण शामिल हैं',
addTool: 'उपकरण जोड़ें',
addToolModal: {
type: 'प्रकार',
category: 'श्रेणी',
add: 'जोड़ें',
added: 'जोड़ा गया',
manageInTools: 'उपकरणों में प्रबंधित करें',
custom: {
title: 'कोई कस्टम टूल उपलब्ध नहीं है',
tip: 'एक कस्टम टूल बनाएं',

View File

@ -1,5 +1,6 @@
const translation = {
type: {
all: 'Semua',
workflow: 'Alur Kerja',
builtIn: 'Perkakas',
custom: 'Adat',
@ -34,6 +35,8 @@ const translation = {
category: 'golongan',
type: 'jenis',
added: 'Ditambahkan',
add: 'tambah',
manageInTools: 'Kelola di Alat',
},
createTool: {
exampleOptions: {
@ -237,6 +240,7 @@ const translation = {
title: 'Perkakas',
createCustomTool: 'Buat Alat Kustom',
customToolTip: 'Pelajari alat kustom Dify lebih lanjut',
addTool: 'Tambahkan Alat',
author: 'Oleh',
copyToolName: 'Salin Nama',
howToGet: 'Cara mendapatkan',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'Crea Strumento Personalizzato',
customToolTip: 'Scopri di più sugli strumenti personalizzati di Dify',
type: {
all: 'Tutti',
builtIn: 'Integrato',
custom: 'Personalizzato',
workflow: 'Flusso di lavoro',
@ -21,10 +22,13 @@ const translation = {
'Dopo aver configurato le credenziali, tutti i membri all\'interno del workspace possono utilizzare questo strumento durante l\'orchestrazione delle applicazioni.',
},
includeToolNum: '{{num}} strumenti inclusi',
addTool: 'Aggiungi Strumento',
addToolModal: {
type: 'tipo',
category: 'categoria',
add: 'aggiungi',
added: 'aggiunto',
manageInTools: 'Gestisci in Strumenti',
custom: {
title: 'Nessuno strumento personalizzato disponibile',
tip: 'Crea uno strumento personalizzato',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'カスタムツールを作成する',
customToolTip: 'Dify カスタムツールの詳細',
type: {
all: 'すべて',
builtIn: 'ツール',
custom: 'カスタム',
workflow: 'ワークフロー',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: '資格情報を構成した後、ワークスペース内のすべてのメンバーがアプリケーションのオーケストレーション時にこのツールを使用できます。',
},
includeToolNum: '{{num}}個のツールが含まれています',
addTool: 'ツールを追加する',
addToolModal: {
type: 'タイプ',
category: 'カテゴリー',
add: '追加',
added: '追加済',
manageInTools: 'ツールリストに移動して管理する',
custom: {
title: 'カスタムツールはありません',
tip: 'カスタムツールを作成する',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: '커스텀 도구 만들기',
customToolTip: 'Dify 커스텀 도구에 대해 더 알아보기',
type: {
all: '모두',
builtIn: '내장',
custom: '커스텀',
workflow: '워크플로우',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: '자격 증명을 구성한 후에 워크스페이스의 모든 멤버가 이 도구를 사용하여 애플리케이션을 조작할 수 있습니다.',
},
includeToolNum: '{{num}}개의 도구가 포함되어 있습니다',
addTool: '도구 추가',
addToolModal: {
type: '타입',
category: '카테고리',
add: '추가',
added: '추가됨',
manageInTools: '도구에서 관리',
custom: {
title: '사용자 정의 도구 없음',
tip: '사용자 정의 도구 생성',

View File

@ -2,6 +2,7 @@ const translation = {
title: 'Narzędzia',
createCustomTool: 'Utwórz niestandardowe narzędzie',
type: {
all: 'Wszystkie',
builtIn: 'Wbudowane',
custom: 'Niestandardowe',
workflow: 'Przepływ pracy',
@ -20,6 +21,7 @@ const translation = {
'Po skonfigurowaniu poświadczeń wszyscy członkowie w przestrzeni roboczej mogą używać tego narzędzia podczas projektowania aplikacji.',
},
includeToolNum: '{{num}} narzędzi zawarte',
addTool: 'Dodaj narzędzie',
createTool: {
title: 'Utwórz niestandardowe narzędzie',
editAction: 'Konfiguruj',
@ -143,9 +145,11 @@ const translation = {
notAuthorized: 'Narzędzie nieautoryzowane',
howToGet: 'Jak uzyskać',
addToolModal: {
manageInTools: 'Zarządzanie w Narzędziach',
added: 'Dodane',
type: 'typ',
category: 'kategoria',
add: 'dodawać',
custom: {
title: 'Brak dostępnego narzędzia niestandardowego',
tip: 'Utwórz narzędzie niestandardowe',

View File

@ -2,6 +2,7 @@ const translation = {
title: 'Ferramentas',
createCustomTool: 'Criar Ferramenta Personalizada',
type: {
all: 'Todas',
builtIn: 'Integradas',
custom: 'Personalizadas',
workflow: 'Fluxo de trabalho',
@ -19,6 +20,7 @@ const translation = {
setupModalTitleDescription: 'Após configurar as credenciais, todos os membros do espaço de trabalho podem usar essa ferramenta ao orquestrar aplicativos.',
},
includeToolNum: '{{num}} ferramentas incluídas',
addTool: 'Adicionar Ferramenta',
createTool: {
title: 'Criar Ferramenta Personalizada',
editAction: 'Configurar',
@ -141,7 +143,9 @@ const translation = {
addToolModal: {
category: 'categoria',
type: 'tipo',
add: 'adicionar',
added: 'Adicionado',
manageInTools: 'Gerenciar em Ferramentas',
custom: {
title: 'Nenhuma ferramenta personalizada disponível',
tip: 'Crie uma ferramenta personalizada',

View File

@ -2,6 +2,7 @@ const translation = {
title: 'Instrumente',
createCustomTool: 'Creează Instrument Personalizat',
type: {
all: 'Toate',
builtIn: 'Incorporat',
custom: 'Personalizat',
workflow: 'Flux de lucru',
@ -19,6 +20,7 @@ const translation = {
setupModalTitleDescription: 'După configurarea credențialelor, toți membrii din spațiul de lucru pot utiliza acest instrument la orchestrarea aplicațiilor.',
},
includeToolNum: '{{num}} instrumente incluse',
addTool: 'Adaugă Instrument',
createTool: {
title: 'Creează Instrument Personalizat',
editAction: 'Configurează',
@ -141,6 +143,8 @@ const translation = {
addToolModal: {
added: 'adăugat',
category: 'categorie',
manageInTools: 'Gestionați în Instrumente',
add: 'adăuga',
type: 'tip',
custom: {
title: 'Niciun instrument personalizat disponibil',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'Создать пользовательский инструмент',
customToolTip: 'Узнать больше о пользовательских инструментах Dify',
type: {
all: 'Все',
builtIn: 'Встроенные',
custom: 'Пользовательские',
workflow: 'Рабочий процесс',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: 'После настройки учетных данных все участники рабочего пространства смогут использовать этот инструмент при оркестровке приложений.',
},
includeToolNum: 'Включено {{num}} инструментов',
addTool: 'Добавить инструмент',
addToolModal: {
type: 'тип',
category: 'категория',
add: 'добавить',
added: 'добавлено',
manageInTools: 'Управлять в инструментах',
custom: {
title: 'Нет доступного пользовательского инструмента',
tip: 'Создать пользовательский инструмент',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'Ustvari prilagojeno orodje',
customToolTip: 'Izvedite več o prilagojenih orodjih Dify',
type: {
all: 'Vsa',
builtIn: 'Vgrajena',
custom: 'Prilagojena',
workflow: 'Potek dela',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: 'Po konfiguraciji poverilnic bodo vsi člani znotraj delovnega prostora lahko uporabljali to orodje pri orkestraciji aplikacij.',
},
includeToolNum: 'Vključeno {{num}} orodij',
addTool: 'Dodaj orodje',
addToolModal: {
type: 'tip',
category: 'kategorija',
add: 'dodaj',
added: 'dodano',
manageInTools: 'Upravljaj v Orodjih',
custom: {
title: 'Žiadne prispôsobené nástroje nie sú k dispozícii',
tip: 'Vytvorte prispôsobený nástroj',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'สร้างเครื่องมือที่กําหนดเอง',
customToolTip: 'เรียนรู้เพิ่มเติมเกี่ยวกับเครื่องมือแบบกําหนดเองของ Dify',
type: {
all: 'ทั้งหมด',
builtIn: 'ในตัว',
custom: 'ธรรมเนียม',
workflow: 'เวิร์กโฟลว์',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: 'หลังจากกําหนดค่าข้อมูลประจําตัวแล้ว สมาชิกทั้งหมดภายในพื้นที่ทํางานสามารถใช้เครื่องมือนี้เมื่อประสานงานแอปพลิเคชันได้',
},
includeToolNum: '{{num}} รวมเครื่องมือ',
addTool: 'เพิ่มเครื่องมือ',
addToolModal: {
type: 'ประเภท',
category: 'ประเภท',
add: 'เพิ่ม',
added: 'เพิ่ม',
manageInTools: 'จัดการในเครื่องมือ',
custom: {
title: 'ไม่มีเครื่องมือกำหนดเอง',
tip: 'สร้างเครื่องมือกำหนดเอง',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: 'Özel Araç Oluştur',
customToolTip: 'Dify özel araçları hakkında daha fazla bilgi edinin',
type: {
all: 'Hepsi',
builtIn: 'Yerleşik',
custom: 'Özel',
workflow: 'Workflow',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: 'Kimlik bilgilerini yapılandırdıktan sonra, çalışma alanındaki tüm üyeler uygulamaları düzenlerken bu aracı kullanabilir.',
},
includeToolNum: '{{num}} araç dahil',
addTool: 'Araç Ekle',
addToolModal: {
type: 'Tür',
category: 'Kategori',
add: 'Ekle',
added: 'Eklendi',
manageInTools: 'Araçlarda Yönet',
custom: {
title: 'Mevcut özel araç yok',
tip: 'Özel bir araç oluşturun',

View File

@ -2,6 +2,7 @@ const translation = {
title: 'Інструменти',
createCustomTool: 'Створити власний інструмент',
type: {
all: 'Усі',
builtIn: 'Вбудовані',
custom: 'Користувацькі',
workflow: 'Робочий процес',
@ -19,6 +20,7 @@ const translation = {
setupModalTitleDescription: 'Після налаштування облікових даних усі члени робочого простору можуть використовувати цей інструмент під час оркестрування програм.',
},
includeToolNum: '{{num}} інструмент(ів) включено',
addTool: 'Додати інструмент ',
createTool: {
title: 'Створити власний інструмент',
editAction: 'Налаштування',
@ -140,8 +142,10 @@ const translation = {
howToGet: 'Як отримати',
addToolModal: {
category: 'категорія',
add: 'Додати',
added: 'Додано',
type: 'тип',
manageInTools: 'Керування в інструментах',
custom: {
title: 'Немає доступного користувацького інструмента',
tip: 'Створити користувацький інструмент',

View File

@ -2,6 +2,7 @@ const translation = {
title: 'Công cụ',
createCustomTool: 'Tạo công cụ tùy chỉnh',
type: {
all: 'Tất cả',
builtIn: 'Tích hợp sẵn',
custom: 'Tùy chỉnh',
workflow: 'Quy trình làm việc',
@ -19,6 +20,7 @@ const translation = {
setupModalTitleDescription: 'Sau khi cấu hình thông tin đăng nhập, tất cả thành viên trong không gian làm việc có thể sử dụng công cụ này khi triển khai ứng dụng.',
},
includeToolNum: 'Bao gồm {{num}} công cụ',
addTool: 'Thêm công cụ',
createTool: {
title: 'Tạo công cụ tùy chỉnh',
editAction: 'Cấu hình',
@ -140,7 +142,9 @@ const translation = {
howToGet: 'Cách nhận',
addToolModal: {
category: 'loại',
manageInTools: 'Quản lý trong Công cụ',
type: 'kiểu',
add: 'thêm',
added: 'Thêm',
custom: {
title: 'Không có công cụ tùy chỉnh nào',

View File

@ -31,7 +31,7 @@ const translation = {
},
completionMode: {
title: '文本生成型应用 API',
info: '可用于生成高质量文本的应用,例如生成文章、摘要、翻译等,通过调用 completion-messages 接口,发送用户输入得到生成文本结果。用于生成文本的模型参数和提示词模取决于开发者在 Dify 提示词编排页的设置。',
info: '可用于生成高质量文本的应用,例如生成文章、摘要、翻译等,通过调用 completion-messages 接口,发送用户输入得到生成文本结果。用于生成文本的模型参数和提示词模取决于开发者在 Dify 提示词编排页的设置。',
createCompletionApi: '创建文本补全消息',
createCompletionApiTip: '创建文本补全消息,支持一问一答模式。',
inputsTips: '选填以键值对方式提供用户输入字段与提示词编排中的变量对应。Key 为变量名称Value 是参数值。如果字段类型为 Select传入的 Value 需为预设选项之一。',

View File

@ -38,7 +38,7 @@ const translation = {
newApp: {
learnMore: '了解更多',
startFromBlank: '创建空白应用',
startFromTemplate: '从应用模创建',
startFromTemplate: '从应用模创建',
foundResult: '{{count}} 个结果',
foundResults: '{{count}} 个结果',
noAppsFound: '未找到应用',
@ -80,7 +80,7 @@ const translation = {
Confirm: '确认',
import: '导入',
nameNotEmpty: '名称不能为空',
appTemplateNotSelected: '请选择应用模',
appTemplateNotSelected: '请选择应用模',
appTypeRequired: '请选择应用类型',
appCreated: '应用已创建',
caution: '注意',
@ -95,7 +95,7 @@ const translation = {
},
newAppFromTemplate: {
byCategories: '分类',
searchAllTemplate: '搜索所有模...',
searchAllTemplate: '搜索所有模...',
sidebar: {
Recommended: '推荐',
Agent: 'Agent',

View File

@ -3,6 +3,7 @@ const translation = {
createCustomTool: '创建自定义工具',
customToolTip: '了解更多关于 Dify 自定义工具的信息',
type: {
all: '全部',
builtIn: '工具',
custom: '自定义',
workflow: '工作流',
@ -20,10 +21,13 @@ const translation = {
setupModalTitleDescription: '配置凭据后,工作区中的所有成员都可以在编排应用程序时使用此工具。',
},
includeToolNum: '包含 {{num}} 个 {{action}}',
addTool: '添加工具',
addToolModal: {
type: '类型',
category: '类别',
add: '添加',
added: '已添加',
manageInTools: '去工具列表管理',
custom: {
title: '没有可用的自定义工具',
tip: '创建自定义工具',
@ -61,7 +65,7 @@ const translation = {
exampleOptions: {
json: '天气 (JSON)',
yaml: '宠物商店 (YAML)',
blankTemplate: '空白模',
blankTemplate: '空白模',
},
availableTools: {
title: '可用工具',

View File

@ -2,6 +2,7 @@ const translation = {
title: '工具',
createCustomTool: '建立自定義工具',
type: {
all: '全部',
builtIn: '內建',
custom: '自定義',
workflow: '工作流',
@ -19,6 +20,7 @@ const translation = {
setupModalTitleDescription: '配置憑據後,工作區中的所有成員都可以在編排應用程式時使用此工具。',
},
includeToolNum: '包含 {{num}} 個工具',
addTool: '新增工具',
createTool: {
title: '建立自定義工具',
editAction: '編輯',
@ -139,8 +141,10 @@ const translation = {
notAuthorized: '工具未授權',
howToGet: '如何獲取',
addToolModal: {
add: '加',
type: '類型',
added: '新增',
manageInTools: '在工具中管理',
category: '類別',
custom: {
title: '沒有可用的自訂工具',

View File

@ -1,7 +1,7 @@
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com https://api2.amplitude.com *.amplitude.com'
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com'
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking

View File

@ -1,8 +1,8 @@
{
"name": "dify-web",
"version": "1.10.0",
"version": "1.10.0-rc1",
"private": true,
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c",
"packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8",
"engines": {
"node": ">=v22.11.0"
},
@ -44,8 +44,6 @@
"knip": "knip"
},
"dependencies": {
"@amplitude/analytics-browser": "^2.31.3",
"@amplitude/plugin-session-replay-browser": "^1.23.6",
"@emoji-mart/data": "^1.2.1",
"@floating-ui/react": "^0.26.28",
"@formatjs/intl-localematcher": "^0.5.10",

3414
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,8 @@ import type {
WorkflowToolProviderRequest,
WorkflowToolProviderResponse,
} from '@/app/components/tools/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { Label } from '@/app/components/tools/labels/constant'
import { buildProviderQuery } from './_tools_util'
export const fetchCollectionList = () => {
@ -110,6 +112,26 @@ export const testAPIAvailable = (payload: any) => {
})
}
export const fetchAllBuiltInTools = () => {
return get<ToolWithProvider[]>('/workspaces/current/tools/builtin')
}
export const fetchAllCustomTools = () => {
return get<ToolWithProvider[]>('/workspaces/current/tools/api')
}
export const fetchAllWorkflowTools = () => {
return get<ToolWithProvider[]>('/workspaces/current/tools/workflow')
}
export const fetchAllMCPTools = () => {
return get<ToolWithProvider[]>('/workspaces/current/tools/mcp')
}
export const fetchLabelList = () => {
return get<Label[]>('/workspaces/current/tool-labels')
}
export const createWorkflowToolProvider = (payload: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
return post('/workspaces/current/tool-provider/workflow/create', {
body: { ...payload },

Some files were not shown because too many files have changed in this diff Show More