Merge branch 'feat/webapp-verified-sso-main' into deploy/enterprise

This commit is contained in:
GareArc 2025-06-04 00:12:57 +09:00
commit df04c1e0a3
No known key found for this signature in database
235 changed files with 2991 additions and 1065 deletions

View File

@ -1,12 +1,13 @@
#!/bin/bash
npm add -g pnpm@10.8.0
npm add -g pnpm@10.11.1
cd web && pnpm install
pipx install uv
echo 'alias start-api="cd /workspaces/dify/api && uv run python -m flask run --host 0.0.0.0 --port=5001 --debug"' >> ~/.bashrc
echo 'alias start-worker="cd /workspaces/dify/api && uv run python -m celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail,ops_trace,app_deletion"' >> ~/.bashrc
echo 'alias start-web="cd /workspaces/dify/web && pnpm dev"' >> ~/.bashrc
echo 'alias start-web-prod="cd /workspaces/dify/web && pnpm build && pnpm start"' >> ~/.bashrc
echo 'alias start-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env up -d"' >> ~/.bashrc
echo 'alias stop-containers="cd /workspaces/dify/docker && docker-compose -f docker-compose.middleware.yaml -p dify --env-file middleware.env down"' >> ~/.bashrc

View File

@ -1,25 +1,23 @@
# Summary
> [!IMPORTANT]
>
> 1. Make sure you have read our [contribution guidelines](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)
> 2. Ensure there is an associated issue and you have been assigned to it
> 3. Use the correct syntax to link this PR: `Fixes #<issue number>`.
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
## Summary
> [!Tip]
> Close issue syntax: `Fixes #<issue number>` or `Resolves #<issue number>`, see [documentation](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) for more details.
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
# Screenshots
## Screenshots
| Before | After |
|--------|-------|
| ... | ... |
# Checklist
> [!IMPORTANT]
> Please review the checklist below before submitting your pull request.
## Checklist
- [ ] This change requires a documentation update, included: [Dify Document](https://github.com/langgenius/dify-docs)
- [x] I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
- [x] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
- [x] I've updated the documentation accordingly.
- [x] I ran `dev/reformat`(backend) and `cd web && npx lint-staged`(frontend) to appease the lint gods

View File

@ -31,11 +31,19 @@ jobs:
echo "FILES_CHANGED=false" >> $GITHUB_ENV
fi
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- name: Set up Node.js
if: env.FILES_CHANGED == 'true'
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: pnpm
cache-dependency-path: ./web/package.json
- name: Install dependencies
if: env.FILES_CHANGED == 'true'

View File

@ -235,7 +235,7 @@ At the same time, please consider supporting Dify by sharing it on social media
## Community & contact
- [Github Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions.
- [GitHub Discussion](https://github.com/langgenius/dify/discussions). Best for: sharing feedback and asking questions.
- [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
- [Discord](https://discord.gg/FngNHpbcY7). Best for: sharing your applications and hanging out with the community.
- [X(Twitter)](https://twitter.com/dify_ai). Best for: sharing your applications and hanging out with the community.

View File

@ -223,7 +223,7 @@ docker compose up -d
</a>
## المجتمع والاتصال
- [مناقشة Github](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة.
- [مناقشة GitHub](https://github.com/langgenius/dify/discussions). الأفضل لـ: مشاركة التعليقات وطرح الأسئلة.
- [المشكلات على GitHub](https://github.com/langgenius/dify/issues). الأفضل لـ: الأخطاء التي تواجهها في استخدام Dify.AI، واقتراحات الميزات. انظر [دليل المساهمة](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
- [Discord](https://discord.gg/FngNHpbcY7). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.
- [تويتر](https://twitter.com/dify_ai). الأفضل لـ: مشاركة تطبيقاتك والترفيه مع المجتمع.

View File

@ -234,7 +234,7 @@ GitHub-এ ডিফাইকে স্টার দিয়ে রাখুন
## কমিউনিটি এবং যোগাযোগ
- [Github Discussion](https://github.com/langgenius/dify/discussions) ফিডব্যাক এবং প্রতিক্রিয়া জানানোর মাধ্যম।
- [GitHub Discussion](https://github.com/langgenius/dify/discussions) ফিডব্যাক এবং প্রতিক্রিয়া জানানোর মাধ্যম।
- [GitHub Issues](https://github.com/langgenius/dify/issues). Dify.AI ব্যবহার করে আপনি যেসব বাগের সম্মুখীন হন এবং ফিচার প্রস্তাবনা। আমাদের [অবদান নির্দেশিকা](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) দেখুন।
- [Discord](https://discord.gg/FngNHpbcY7) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম।
- [X(Twitter)](https://twitter.com/dify_ai) আপনার এপ্লিকেশন শেয়ার এবং কমিউনিটি আড্ডার মাধ্যম।

View File

@ -243,7 +243,7 @@ docker compose up -d
我们欢迎您为 Dify 做出贡献,以帮助改善 Dify。包括提交代码、问题、新想法或分享您基于 Dify 创建的有趣且有用的 AI 应用程序。同时,我们也欢迎您在不同的活动、会议和社交媒体上分享 Dify。
- [Github Discussion](https://github.com/langgenius/dify/discussions). 👉:分享您的应用程序并与社区交流。
- [GitHub Discussion](https://github.com/langgenius/dify/discussions). 👉:分享您的应用程序并与社区交流。
- [GitHub Issues](https://github.com/langgenius/dify/issues)。👉:使用 Dify.AI 时遇到的错误和问题,请参阅[贡献指南](CONTRIBUTING.md)。
- [电子邮件支持](mailto:hello@dify.ai?subject=[GitHub]Questions%20About%20Dify)。👉:关于使用 Dify.AI 的问题。
- [Discord](https://discord.gg/FngNHpbcY7)。👉:分享您的应用程序并与社区交流。

View File

@ -230,7 +230,7 @@ Falls Sie Code beitragen möchten, lesen Sie bitte unseren [Contribution Guide](
## Gemeinschaft & Kontakt
* [Github Discussion](https://github.com/langgenius/dify/discussions). Am besten geeignet für: den Austausch von Feedback und das Stellen von Fragen.
* [GitHub Discussion](https://github.com/langgenius/dify/discussions). Am besten geeignet für: den Austausch von Feedback und das Stellen von Fragen.
* [GitHub Issues](https://github.com/langgenius/dify/issues). Am besten für: Fehler, auf die Sie bei der Verwendung von Dify.AI stoßen, und Funktionsvorschläge. Siehe unseren [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [Discord](https://discord.gg/FngNHpbcY7). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community.
* [X(Twitter)](https://twitter.com/dify_ai). Am besten geeignet für: den Austausch von Bewerbungen und den Austausch mit der Community.

View File

@ -236,7 +236,7 @@ docker compose up -d
## コミュニティ & お問い合わせ
* [Github Discussion](https://github.com/langgenius/dify/discussions). 主に: フィードバックの共有や質問。
* [GitHub Discussion](https://github.com/langgenius/dify/discussions). 主に: フィードバックの共有や質問。
* [GitHub Issues](https://github.com/langgenius/dify/issues). 主に: Dify.AIを使用する際に発生するエラーや問題については、[貢献ガイド](CONTRIBUTING_JA.md)を参照してください
* [Discord](https://discord.gg/FngNHpbcY7). 主に: アプリケーションの共有やコミュニティとの交流。
* [X(Twitter)](https://twitter.com/dify_ai). 主に: アプリケーションの共有やコミュニティとの交流。

View File

@ -235,7 +235,7 @@ At the same time, please consider supporting Dify by sharing it on social media
## Community & Contact
* [Github Discussion](https://github.com/langgenius/dify/discussions
* [GitHub Discussion](https://github.com/langgenius/dify/discussions
). Best for: sharing feedback and asking questions.
* [GitHub Issues](https://github.com/langgenius/dify/issues). Best for: bugs you encounter using Dify.AI, and feature proposals. See our [Contribution Guide](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).

View File

@ -229,7 +229,7 @@ Dify를 Kubernetes에 배포하고 프리미엄 스케일링 설정을 구성했
## 커뮤니티 & 연락처
* [Github 토론](https://github.com/langgenius/dify/discussions). 피드백 공유 및 질문하기에 적합합니다.
* [GitHub 토론](https://github.com/langgenius/dify/discussions). 피드백 공유 및 질문하기에 적합합니다.
* [GitHub 이슈](https://github.com/langgenius/dify/issues). Dify.AI 사용 중 발견한 버그와 기능 제안에 적합합니다. [기여 가이드](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)를 참조하세요.
* [디스코드](https://discord.gg/FngNHpbcY7). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다.
* [트위터](https://twitter.com/dify_ai). 애플리케이션 공유 및 커뮤니티와 소통하기에 적합합니다.

View File

@ -229,7 +229,7 @@ Za tiste, ki bi radi prispevali kodo, si oglejte naš vodnik za prispevke . Hkra
## Skupnost in stik
* [Github Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj.
* [GitHub Discussion](https://github.com/langgenius/dify/discussions). Najboljše za: izmenjavo povratnih informacij in postavljanje vprašanj.
* [GitHub Issues](https://github.com/langgenius/dify/issues). Najboljše za: hrošče, na katere naletite pri uporabi Dify.AI, in predloge funkcij. Oglejte si naš [vodnik za prispevke](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md).
* [Discord](https://discord.gg/FngNHpbcY7). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo.
* [X(Twitter)](https://twitter.com/dify_ai). Najboljše za: deljenje vaših aplikacij in druženje s skupnostjo.

View File

@ -227,7 +227,7 @@ Aynı zamanda, lütfen Dify'ı sosyal medyada, etkinliklerde ve konferanslarda p
## Topluluk & iletişim
* [Github Tartışmaları](https://github.com/langgenius/dify/discussions). En uygun: geri bildirim paylaşmak ve soru sormak için.
* [GitHub Tartışmaları](https://github.com/langgenius/dify/discussions). En uygun: geri bildirim paylaşmak ve soru sormak için.
* [GitHub Sorunları](https://github.com/langgenius/dify/issues). En uygun: Dify.AI kullanırken karşılaştığınız hatalar ve özellik önerileri için. [Katkı Kılavuzumuza](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md) bakın.
* [Discord](https://discord.gg/FngNHpbcY7). En uygun: uygulamalarınızı paylaşmak ve toplulukla vakit geçirmek için.
* [X(Twitter)](https://twitter.com/dify_ai). En uygun: uygulamalarınızı paylaşmak ve toplulukla vakit geçirmek için.

View File

@ -233,7 +233,7 @@ Dify 的所有功能都提供相應的 API因此您可以輕鬆地將 Dify
## 社群與聯絡方式
- [Github Discussion](https://github.com/langgenius/dify/discussions):最適合分享反饋和提問。
- [GitHub Discussion](https://github.com/langgenius/dify/discussions):最適合分享反饋和提問。
- [GitHub Issues](https://github.com/langgenius/dify/issues):最適合報告使用 Dify.AI 時遇到的問題和提出功能建議。請參閱我們的[貢獻指南](https://github.com/langgenius/dify/blob/main/CONTRIBUTING.md)。
- [Discord](https://discord.gg/FngNHpbcY7):最適合分享您的應用程式並與社群互動。
- [X(Twitter)](https://twitter.com/dify_ai):最適合分享您的應用程式並與社群互動。

View File

@ -127,6 +127,16 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded:
end_user = None
if end_user_id:
end_user = db.session.query(EndUser).filter(EndUser.id == end_user_id).first()
if session_id:
end_user = (
db.session.query(EndUser)
.filter(
EndUser.session_id == session_id,
EndUser.tenant_id == app_model.tenant_id,
EndUser.app_id == app_model.id,
)
.first()
)
if not end_user:
if not session_id:
raise NotFound("Missing session_id for existing web user.")

View File

@ -1,193 +0,0 @@
import logging
from threading import Thread
from typing import Optional, Union
from flask import Flask, current_app
from configs import dify_config
from core.app.entities.app_invoke_entities import (
AdvancedChatAppGenerateEntity,
AgentChatAppGenerateEntity,
ChatAppGenerateEntity,
CompletionAppGenerateEntity,
)
from core.app.entities.queue_entities import (
QueueAnnotationReplyEvent,
QueueMessageFileEvent,
QueueRetrieverResourcesEvent,
)
from core.app.entities.task_entities import (
EasyUITaskState,
MessageFileStreamResponse,
MessageReplaceStreamResponse,
MessageStreamResponse,
WorkflowTaskState,
)
from core.llm_generator.llm_generator import LLMGenerator
from core.tools.signature import sign_tool_file
from extensions.ext_database import db
from models.model import AppMode, Conversation, MessageAnnotation, MessageFile
from services.annotation_service import AppAnnotationService
class MessageCycleManage:
def __init__(
self,
*,
application_generate_entity: Union[
ChatAppGenerateEntity,
CompletionAppGenerateEntity,
AgentChatAppGenerateEntity,
AdvancedChatAppGenerateEntity,
],
task_state: Union[EasyUITaskState, WorkflowTaskState],
) -> None:
self._application_generate_entity = application_generate_entity
self._task_state = task_state
def _generate_conversation_name(self, *, conversation_id: str, query: str) -> Optional[Thread]:
"""
Generate conversation name.
:param conversation_id: conversation id
:param query: query
:return: thread
"""
if isinstance(self._application_generate_entity, CompletionAppGenerateEntity):
return None
is_first_message = self._application_generate_entity.conversation_id is None
extras = self._application_generate_entity.extras
auto_generate_conversation_name = extras.get("auto_generate_conversation_name", True)
if auto_generate_conversation_name and is_first_message:
# start generate thread
thread = Thread(
target=self._generate_conversation_name_worker,
kwargs={
"flask_app": current_app._get_current_object(), # type: ignore
"conversation_id": conversation_id,
"query": query,
},
)
thread.start()
return thread
return None
def _generate_conversation_name_worker(self, flask_app: Flask, conversation_id: str, query: str):
with flask_app.app_context():
# get conversation and message
conversation = db.session.query(Conversation).filter(Conversation.id == conversation_id).first()
if not conversation:
return
if conversation.mode != AppMode.COMPLETION.value:
app_model = conversation.app
if not app_model:
return
# generate conversation name
try:
name = LLMGenerator.generate_conversation_name(app_model.tenant_id, query)
conversation.name = name
except Exception as e:
if dify_config.DEBUG:
logging.exception(f"generate conversation name failed, conversation_id: {conversation_id}")
pass
db.session.merge(conversation)
db.session.commit()
db.session.close()
def _handle_annotation_reply(self, event: QueueAnnotationReplyEvent) -> Optional[MessageAnnotation]:
"""
Handle annotation reply.
:param event: event
:return:
"""
annotation = AppAnnotationService.get_annotation_by_id(event.message_annotation_id)
if annotation:
account = annotation.account
self._task_state.metadata["annotation_reply"] = {
"id": annotation.id,
"account": {"id": annotation.account_id, "name": account.name if account else "Dify user"},
}
return annotation
return None
def _handle_retriever_resources(self, event: QueueRetrieverResourcesEvent) -> None:
"""
Handle retriever resources.
:param event: event
:return:
"""
if self._application_generate_entity.app_config.additional_features.show_retrieve_source:
self._task_state.metadata["retriever_resources"] = event.retriever_resources
def _message_file_to_stream_response(self, event: QueueMessageFileEvent) -> Optional[MessageFileStreamResponse]:
"""
Message file to stream response.
:param event: event
:return:
"""
message_file = db.session.query(MessageFile).filter(MessageFile.id == event.message_file_id).first()
if message_file and message_file.url is not None:
# get tool file id
tool_file_id = message_file.url.split("/")[-1]
# trim extension
tool_file_id = tool_file_id.split(".")[0]
# get extension
if "." in message_file.url:
extension = f".{message_file.url.split('.')[-1]}"
if len(extension) > 10:
extension = ".bin"
else:
extension = ".bin"
# add sign url to local file
if message_file.url.startswith("http"):
url = message_file.url
else:
url = sign_tool_file(tool_file_id=tool_file_id, extension=extension)
return MessageFileStreamResponse(
task_id=self._application_generate_entity.task_id,
id=message_file.id,
type=message_file.type,
belongs_to=message_file.belongs_to or "user",
url=url,
)
return None
def _message_to_stream_response(
self, answer: str, message_id: str, from_variable_selector: Optional[list[str]] = None
) -> MessageStreamResponse:
"""
Message to stream response.
:param answer: answer
:param message_id: message id
:return:
"""
return MessageStreamResponse(
task_id=self._application_generate_entity.task_id,
id=message_id,
answer=answer,
from_variable_selector=from_variable_selector,
)
def _message_replace_to_stream_response(self, answer: str, reason: str = "") -> MessageReplaceStreamResponse:
"""
Message replace to stream response.
:param answer: answer
:return:
"""
return MessageReplaceStreamResponse(
task_id=self._application_generate_entity.task_id, answer=answer, reason=reason
)

View File

@ -1,98 +0,0 @@
"""
Domain entities for workflow node execution.
This module contains the domain model for workflow node execution, which is used
by the core workflow module. These models are independent of the storage mechanism
and don't contain implementation details like tenant_id, app_id, etc.
"""
from collections.abc import Mapping
from datetime import datetime
from enum import StrEnum
from typing import Any, Optional
from pydantic import BaseModel, Field
from core.workflow.entities.node_entities import NodeRunMetadataKey
from core.workflow.nodes.enums import NodeType
class NodeExecutionStatus(StrEnum):
"""
Node Execution Status Enum.
"""
RUNNING = "running"
SUCCEEDED = "succeeded"
FAILED = "failed"
EXCEPTION = "exception"
RETRY = "retry"
class NodeExecution(BaseModel):
"""
Domain model for workflow node execution.
This model represents the core business entity of a node execution,
without implementation details like tenant_id, app_id, etc.
Note: User/context-specific fields (triggered_from, created_by, created_by_role)
have been moved to the repository implementation to keep the domain model clean.
These fields are still accepted in the constructor for backward compatibility,
but they are not stored in the model.
"""
# Core identification fields
id: str # Unique identifier for this execution record
node_execution_id: Optional[str] = None # Optional secondary ID for cross-referencing
workflow_id: str # ID of the workflow this node belongs to
workflow_run_id: Optional[str] = None # ID of the specific workflow run (null for single-step debugging)
# Execution positioning and flow
index: int # Sequence number for ordering in trace visualization
predecessor_node_id: Optional[str] = None # ID of the node that executed before this one
node_id: str # ID of the node being executed
node_type: NodeType # Type of node (e.g., start, llm, knowledge)
title: str # Display title of the node
# Execution data
inputs: Optional[Mapping[str, Any]] = None # Input variables used by this node
process_data: Optional[Mapping[str, Any]] = None # Intermediate processing data
outputs: Optional[Mapping[str, Any]] = None # Output variables produced by this node
# Execution state
status: NodeExecutionStatus = NodeExecutionStatus.RUNNING # Current execution status
error: Optional[str] = None # Error message if execution failed
elapsed_time: float = Field(default=0.0) # Time taken for execution in seconds
# Additional metadata
metadata: Optional[Mapping[NodeRunMetadataKey, Any]] = None # Execution metadata (tokens, cost, etc.)
# Timing information
created_at: datetime # When execution started
finished_at: Optional[datetime] = None # When execution completed
def update_from_mapping(
self,
inputs: Optional[Mapping[str, Any]] = None,
process_data: Optional[Mapping[str, Any]] = None,
outputs: Optional[Mapping[str, Any]] = None,
metadata: Optional[Mapping[NodeRunMetadataKey, Any]] = None,
) -> None:
"""
Update the model from mappings.
Args:
inputs: The inputs to update
process_data: The process data to update
outputs: The outputs to update
metadata: The metadata to update
"""
if inputs is not None:
self.inputs = dict(inputs)
if process_data is not None:
self.process_data = dict(process_data)
if outputs is not None:
self.outputs = dict(outputs)
if metadata is not None:
self.metadata = dict(metadata)

View File

@ -1,91 +0,0 @@
"""
Domain entities for workflow execution.
Models are independent of the storage mechanism and don't contain
implementation details like tenant_id, app_id, etc.
"""
from collections.abc import Mapping
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any, Optional
from pydantic import BaseModel, Field
class WorkflowType(StrEnum):
"""
Workflow Type Enum for domain layer
"""
WORKFLOW = "workflow"
CHAT = "chat"
class WorkflowExecutionStatus(StrEnum):
RUNNING = "running"
SUCCEEDED = "succeeded"
FAILED = "failed"
STOPPED = "stopped"
PARTIAL_SUCCEEDED = "partial-succeeded"
class WorkflowExecution(BaseModel):
"""
Domain model for workflow execution based on WorkflowRun but without
user, tenant, and app attributes.
"""
id: str = Field(...)
workflow_id: str = Field(...)
workflow_version: str = Field(...)
sequence_number: int = Field(...)
type: WorkflowType = Field(...)
graph: Mapping[str, Any] = Field(...)
inputs: Mapping[str, Any] = Field(...)
outputs: Optional[Mapping[str, Any]] = None
status: WorkflowExecutionStatus = WorkflowExecutionStatus.RUNNING
error_message: str = Field(default="")
total_tokens: int = Field(default=0)
total_steps: int = Field(default=0)
exceptions_count: int = Field(default=0)
started_at: datetime = Field(...)
finished_at: Optional[datetime] = None
@property
def elapsed_time(self) -> float:
"""
Calculate elapsed time in seconds.
If workflow is not finished, use current time.
"""
end_time = self.finished_at or datetime.now(UTC).replace(tzinfo=None)
return (end_time - self.started_at).total_seconds()
@classmethod
def new(
cls,
*,
id: str,
workflow_id: str,
sequence_number: int,
type: WorkflowType,
workflow_version: str,
graph: Mapping[str, Any],
inputs: Mapping[str, Any],
started_at: datetime,
) -> "WorkflowExecution":
return WorkflowExecution(
id=id,
workflow_id=workflow_id,
sequence_number=sequence_number,
type=type,
workflow_version=workflow_version,
graph=graph,
inputs=inputs,
status=WorkflowExecutionStatus.RUNNING,
started_at=started_at,
)

View File

@ -1,14 +0,0 @@
"""
Repository interfaces for data access.
This package contains repository interfaces that define the contract
for accessing and manipulating data, regardless of the underlying
storage mechanism.
"""
from core.workflow.repository.workflow_node_execution_repository import OrderConfig, WorkflowNodeExecutionRepository
__all__ = [
"OrderConfig",
"WorkflowNodeExecutionRepository",
]

View File

@ -1,42 +0,0 @@
from typing import Optional, Protocol
from core.workflow.entities.workflow_execution_entities import WorkflowExecution
class WorkflowExecutionRepository(Protocol):
"""
Repository interface for WorkflowExecution.
This interface defines the contract for accessing and manipulating
WorkflowExecution data, regardless of the underlying storage mechanism.
Note: Domain-specific concepts like multi-tenancy (tenant_id), application context (app_id),
and other implementation details should be handled at the implementation level, not in
the core interface. This keeps the core domain model clean and independent of specific
application domains or deployment scenarios.
"""
def save(self, execution: WorkflowExecution) -> None:
"""
Save or update a WorkflowExecution instance.
This method handles both creating new records and updating existing ones.
The implementation should determine whether to create or update based on
the execution's ID or other identifying fields.
Args:
execution: The WorkflowExecution instance to save or update
"""
...
def get(self, execution_id: str) -> Optional[WorkflowExecution]:
"""
Retrieve a WorkflowExecution by its ID.
Args:
execution_id: The workflow execution ID
Returns:
The WorkflowExecution instance if found, None otherwise
"""
...

View File

@ -1,92 +0,0 @@
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Literal, Optional, Protocol
from core.workflow.entities.node_execution_entities import NodeExecution
@dataclass
class OrderConfig:
"""Configuration for ordering NodeExecution instances."""
order_by: list[str]
order_direction: Optional[Literal["asc", "desc"]] = None
class WorkflowNodeExecutionRepository(Protocol):
"""
Repository interface for NodeExecution.
This interface defines the contract for accessing and manipulating
NodeExecution data, regardless of the underlying storage mechanism.
Note: Domain-specific concepts like multi-tenancy (tenant_id), application context (app_id),
and trigger sources (triggered_from) should be handled at the implementation level, not in
the core interface. This keeps the core domain model clean and independent of specific
application domains or deployment scenarios.
"""
def save(self, execution: NodeExecution) -> None:
"""
Save or update a NodeExecution instance.
This method handles both creating new records and updating existing ones.
The implementation should determine whether to create or update based on
the execution's ID or other identifying fields.
Args:
execution: The NodeExecution instance to save or update
"""
...
def get_by_node_execution_id(self, node_execution_id: str) -> Optional[NodeExecution]:
"""
Retrieve a NodeExecution by its node_execution_id.
Args:
node_execution_id: The node execution ID
Returns:
The NodeExecution instance if found, None otherwise
"""
...
def get_by_workflow_run(
self,
workflow_run_id: str,
order_config: Optional[OrderConfig] = None,
) -> Sequence[NodeExecution]:
"""
Retrieve all NodeExecution instances for a specific workflow run.
Args:
workflow_run_id: The workflow run ID
order_config: Optional configuration for ordering results
order_config.order_by: List of fields to order by (e.g., ["index", "created_at"])
order_config.order_direction: Direction to order ("asc" or "desc")
Returns:
A list of NodeExecution instances
"""
...
def get_running_executions(self, workflow_run_id: str) -> Sequence[NodeExecution]:
"""
Retrieve all running NodeExecution instances for a specific workflow run.
Args:
workflow_run_id: The workflow run ID
Returns:
A list of running NodeExecution instances
"""
...
def clear(self) -> None:
"""
Clear all NodeExecution records based on implementation-specific criteria.
This method is intended to be used for bulk deletion operations, such as removing
all records associated with a specific app_id and tenant_id in multi-tenant implementations.
"""
...

View File

@ -1,7 +0,0 @@
from core.helper.marketplace import download_plugin_pkg
def test_download_plugin_pkg():
pkg = download_plugin_pkg("langgenius/bing:0.0.1@e58735424d2104f208c2bd683c5142e0332045b425927067acf432b26f3d970b")
assert pkg is not None
assert len(pkg) > 0

View File

@ -412,6 +412,7 @@ QDRANT_API_KEY=difyai123456
QDRANT_CLIENT_TIMEOUT=20
QDRANT_GRPC_ENABLED=false
QDRANT_GRPC_PORT=6334
QDRANT_REPLICATION_FACTOR=1
# Milvus configuration. Only available when VECTOR_STORE is `milvus`.
# The milvus uri.
@ -801,7 +802,7 @@ MAX_TOOLS_NUM=10
MAX_PARALLEL_LIMIT=10
# The maximum number of iterations for agent setting
MAX_ITERATIONS_NUM=5
MAX_ITERATIONS_NUM=99
# ------------------------------
# Environment Variables for web Service

View File

@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.4.0
image: langgenius/dify-api:1.4.1
restart: always
environment:
# Use the shared environment variables.
@ -31,7 +31,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.4.0
image: langgenius/dify-api:1.4.1
restart: always
environment:
# Use the shared environment variables.
@ -57,7 +57,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.4.0
image: langgenius/dify-web:1.4.1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -75,7 +75,7 @@ services:
LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100}
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99}
ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true}
ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true}
ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true}
@ -142,7 +142,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.10-local
image: langgenius/dify-plugin-daemon:0.1.1-local
restart: always
environment:
# Use the shared environment variables.

View File

@ -71,7 +71,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.10-local
image: langgenius/dify-plugin-daemon:0.1.1-local
restart: always
env_file:
- ./middleware.env

View File

@ -138,6 +138,7 @@ x-shared-env: &shared-api-worker-env
QDRANT_CLIENT_TIMEOUT: ${QDRANT_CLIENT_TIMEOUT:-20}
QDRANT_GRPC_ENABLED: ${QDRANT_GRPC_ENABLED:-false}
QDRANT_GRPC_PORT: ${QDRANT_GRPC_PORT:-6334}
QDRANT_REPLICATION_FACTOR: ${QDRANT_REPLICATION_FACTOR:-1}
MILVUS_URI: ${MILVUS_URI:-http://host.docker.internal:19530}
MILVUS_DATABASE: ${MILVUS_DATABASE:-}
MILVUS_TOKEN: ${MILVUS_TOKEN:-}
@ -353,7 +354,7 @@ x-shared-env: &shared-api-worker-env
LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100}
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
PGUSER: ${PGUSER:-${DB_USERNAME}}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-${DB_PASSWORD}}
@ -500,7 +501,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: langgenius/dify-api:1.4.0
image: langgenius/dify-api:1.4.1
restart: always
environment:
# Use the shared environment variables.
@ -529,7 +530,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: langgenius/dify-api:1.4.0
image: langgenius/dify-api:1.4.1
restart: always
environment:
# Use the shared environment variables.
@ -555,7 +556,7 @@ services:
# Frontend web application.
web:
image: langgenius/dify-web:1.4.0
image: langgenius/dify-web:1.4.1
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@ -573,7 +574,7 @@ services:
LOOP_NODE_MAX_COUNT: ${LOOP_NODE_MAX_COUNT:-100}
MAX_TOOLS_NUM: ${MAX_TOOLS_NUM:-10}
MAX_PARALLEL_LIMIT: ${MAX_PARALLEL_LIMIT:-10}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-5}
MAX_ITERATIONS_NUM: ${MAX_ITERATIONS_NUM:-99}
ENABLE_WEBSITE_JINAREADER: ${ENABLE_WEBSITE_JINAREADER:-true}
ENABLE_WEBSITE_FIRECRAWL: ${ENABLE_WEBSITE_FIRECRAWL:-true}
ENABLE_WEBSITE_WATERCRAWL: ${ENABLE_WEBSITE_WATERCRAWL:-true}
@ -640,7 +641,7 @@ services:
# plugin daemon
plugin_daemon:
image: langgenius/dify-plugin-daemon:0.0.10-local
image: langgenius/dify-plugin-daemon:0.1.1-local
restart: always
environment:
# Use the shared environment variables.

View File

@ -109,7 +109,7 @@ EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
EXPOSE_PLUGIN_DEBUGGING_PORT=5003
PLUGIN_DIFY_INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
PLUGIN_DIFY_INNER_API_URL=http://api:5001
PLUGIN_DIFY_INNER_API_URL=http://host.docker.internal:5001
MARKETPLACE_ENABLED=true
MARKETPLACE_API_URL=https://marketplace.dify.ai

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -50,7 +50,7 @@ NEXT_PUBLIC_MAX_TOOLS_NUM=10
NEXT_PUBLIC_MAX_PARALLEL_LIMIT=10
# The maximum number of iterations for agent setting
NEXT_PUBLIC_MAX_ITERATIONS_NUM=5
NEXT_PUBLIC_MAX_ITERATIONS_NUM=99
NEXT_PUBLIC_ENABLE_WEBSITE_JINAREADER=true
NEXT_PUBLIC_ENABLE_WEBSITE_FIRECRAWL=true

View File

@ -6,7 +6,7 @@ LABEL maintainer="takatost@gmail.com"
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache tzdata
RUN npm install -g pnpm@10.8.0
RUN npm install -g pnpm@10.11.1
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

View File

@ -2,7 +2,7 @@
import { useContext, useContextSelector } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react'
import cn from '@/utils/classnames'
@ -35,6 +35,7 @@ import Tooltip from '@/app/components/base/tooltip'
import AccessControl from '@/app/components/app/app-access-control'
import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { formatTime } from '@/utils/time'
export type AppCardProps = {
app: App
@ -296,6 +297,15 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
setTags(app.tags)
}, [app.tags])
const EditTimeText = useMemo(() => {
const timeText = formatTime({
date: (app.updated_at || app.created_at) * 1000,
dateFormat: 'MM/DD/YYYY h:mm',
})
return `${t('datasetDocuments.segment.editedAt')} ${timeText}`
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [app.updated_at, app.created_at])
return (
<>
<div
@ -320,12 +330,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
<div className='truncate' title={app.name}>{app.name}</div>
</div>
<div className='flex items-center text-[10px] font-medium leading-[18px] text-text-tertiary'>
{app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.advanced').toUpperCase()}</div>}
{app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
{app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
{app.mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
<div className='flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary'>
<div className='truncate' title={app.author_name}>{app.author_name}</div>
<div>·</div>
<div className='truncate'>{EditTimeText}</div>
</div>
</div>
<div className='flex h-5 w-5 shrink-0 items-center justify-center'>

View File

@ -111,7 +111,7 @@ const DatasetCard = ({
return (
<>
<div
className='group relative col-span-1 flex min-h-[160px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg'
className='group relative col-span-1 flex min-h-[171px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg'
data-disable-nprogress={true}
onClick={(e) => {
e.preventDefault()

View File

@ -1298,6 +1298,76 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
method='GET'
title='Get a Chunk Details in a Document'
name='#view_document_chunk'
/>
<Row>
<Col>
Get details of a specific document segment in the specified knowledge base
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
Knowledge Base ID
</Property>
<Property name='document_id' type='string' key='document_id'>
Document ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
Segment ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": {
"id": "chunk_id",
"position": 2,
"document_id": "document_id",
"content": "Segment content text",
"sign_content": "Signature content text",
"answer": "Answer content (if in Q&A mode)",
"word_count": 470,
"tokens": 382,
"keywords": ["keyword1", "keyword2"],
"index_node_id": "index_node_id",
"index_node_hash": "index_node_hash",
"hit_count": 0,
"enabled": true,
"status": "completed",
"created_by": "creator_id",
"created_at": creation_timestamp,
"updated_at": update_timestamp,
"indexing_at": indexing_timestamp,
"completed_at": completion_timestamp,
"error": null,
"child_chunks": []
},
"doc_form": "text_model"
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
method='DELETE'
@ -1771,20 +1841,45 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
Query keyword
</Property>
<Property name='retrieval_model' type='object' key='retrieval_model'>
Retrieval model (optional, if not filled, it will be recalled according to the default method)
- <code>search_method</code> (text) Search method: One of the following four keywords is required
- <code>keyword_search</code> Keyword search
- <code>semantic_search</code> Semantic search
- <code>full_text_search</code> Full-text search
- <code>hybrid_search</code> Hybrid search
- <code>reranking_enable</code> (bool) Whether to enable reranking, required if the search mode is semantic_search or hybrid_search (optional)
- <code>reranking_mode</code> (object) Rerank model configuration, required if reranking is enabled
- <code>reranking_provider_name</code> (string) Rerank model provider
- <code>reranking_model_name</code> (string) Rerank model name
- <code>weights</code> (float) Semantic search weight setting in hybrid search mode
- <code>top_k</code> (integer) Number of results to return (optional)
- <code>score_threshold_enabled</code> (bool) Whether to enable score threshold
- <code>score_threshold</code> (float) Score threshold
Retrieval parameters (optional, if not filled, it will be recalled according to the default method)
- <code>search_method</code> (text) Search method: One of the following four keywords is required
- <code>keyword_search</code> Keyword search
- <code>semantic_search</code> Semantic search
- <code>full_text_search</code> Full-text search
- <code>hybrid_search</code> Hybrid search
- <code>reranking_enable</code> (bool) Whether to enable reranking, required if the search mode is semantic_search or hybrid_search (optional)
- <code>reranking_mode</code> (object) Rerank model configuration, required if reranking is enabled
- <code>reranking_provider_name</code> (string) Rerank model provider
- <code>reranking_model_name</code> (string) Rerank model name
- <code>weights</code> (float) Semantic search weight setting in hybrid search mode
- <code>top_k</code> (integer) Number of results to return (optional)
- <code>score_threshold_enabled</code> (bool) Whether to enable score threshold
- <code>score_threshold</code> (float) Score threshold
- <code>metadata_filtering_conditions</code> (object) Metadata filtering conditions
- <code>logical_operator</code> (string) Logical operator: <code>and</code> | <code>or</code>
- <code>conditions</code> (array[object]) Conditions list
- <code>name</code> (string) Metadata field name
- <code>comparison_operator</code> (string) Comparison operator, allowed values:
- String comparison:
- <code>contains</code>: Contains
- <code>not contains</code>: Does not contain
- <code>start with</code>: Starts with
- <code>end with</code>: Ends with
- <code>is</code>: Equals
- <code>is not</code>: Does not equal
- <code>empty</code>: Is empty
- <code>not empty</code>: Is not empty
- Numeric comparison:
- <code>=</code>: Equals
- <code>≠</code>: Does not equal
- <code>></code>: Greater than
- <code>< </code>: Less than
- <code>≥</code>: Greater than or equal
- <code>≤</code>: Less than or equal
- Time comparison:
- <code>before</code>: Before
- <code>after</code>: After
- <code>value</code> (string|number|null) Comparison value
</Property>
<Property name='external_retrieval_model' type='object' key='external_retrieval_model'>
Unused field
@ -1809,7 +1904,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
"weights": null,
"top_k": 1,
"score_threshold_enabled": false,
"score_threshold": null
"score_threshold": null,
"metadata_filtering_conditions": {
"logical_operator": "and",
"conditions": [
{
"name": "document_name",
"comparison_operator": "contains",
"value": "test"
}
]
}
}
}'`}
>
@ -2089,9 +2194,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
label="/datasets/{dataset_id}/documents/metadata"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/{dataset_id}/documents/metadata' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'\\\n--data-raw '{"operation_data": [{"document_id": "document_id", "metadata_list": [{"id": "id", "value": "value", "name": "name"}]}]}'`}
>
```bash {{ title: 'cURL' }}
```
</CodeGroup>
```bash {{ title: 'cURL' }} </CodeGroup>
</Col>
</Row>
@ -2246,6 +2349,316 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
</Row>
<hr className='ml-0 mr-0' />
Okay, I will translate the Chinese text in your document while keeping all formatting and code content unchanged.
<Heading
url='/datasets/tags'
method='POST'
title='Create New Knowledge Base Type Tag'
name='#create_new_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='name' type='string'>
(text) New tag name, required, maximum length 50
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "testtag1"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"name": "testtag1"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "eddb66c2-04a1-4e3a-8cb2-75abd01e12a6",
"name": "testtag1",
"type": "knowledge",
"binding_count": 0
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='GET'
title='Get Knowledge Base Type Tags'
name='#get_knowledge_type_tags'
/>
<Row>
<Col>
### Request Body
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/tags"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
[
{
"id": "39d6934c-ed36-463d-b4a7-377fa1503dc0",
"name": "testtag1",
"type": "knowledge",
"binding_count": "0"
},
...
]
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='PATCH'
title='Modify Knowledge Base Type Tag Name'
name='#modify_knowledge_tag_name'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='name' type='string'>
(text) Modified tag name, required, maximum length 50
</Property>
<Property name='tag_id' type='string'>
(text) Tag ID, required
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="PATCH"
label="/datasets/tags"
targetCode={`curl --location --request PATCH '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "testtag2", "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}`}
>
```bash {{ title: 'cURL' }}
curl --location --request PATCH '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"name": "testtag2", "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "eddb66c2-04a1-4e3a-8cb2-75abd01e12a6",
"name": "tag-renamed",
"type": "knowledge",
"binding_count": 0
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='DELETE'
title='Delete Knowledge Base Type Tag'
name='#delete_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_id' type='string'>
(text) Tag ID, required
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="DELETE"
label="/datasets/tags"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{ "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}`}
>
```bash {{ title: 'cURL' }}
curl --location --request DELETE '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags/binding'
method='POST'
title='Bind Dataset to Knowledge Base Type Tag'
name='#bind_dataset_to_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_ids' type='list'>
(list) List of Tag IDs, required
</Property>
<Property name='target_id' type='string'>
(text) Dataset ID, required
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags/binding"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags/binding' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"tag_ids": ["65cc29be-d072-4e26-adf4-2f727644da29","1e5348f3-d3ff-42b8-a1b7-0a86d518001a"], "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags/binding' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_ids": ["65cc29be-d072-4e26-adf4-2f727644da29","1e5348f3-d3ff-42b8-a1b7-0a86d518001a"], "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags/unbinding'
method='POST'
title='Unbind Dataset and Knowledge Base Type Tag'
name='#unbind_dataset_and_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_id' type='string'>
(text) Tag ID, required
</Property>
<Property name='target_id' type='string'>
(text) Dataset ID, required
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags/unbinding"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags/unbinding' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"tag_id": "1e5348f3-d3ff-42b8-a1b7-0a86d518001a", "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags/unbinding' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_id": "1e5348f3-d3ff-42b8-a1b7-0a86d518001a", "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/<uuid:dataset_id>/tags'
method='POST'
title='Query Tags Bound to a Dataset'
name='#query_dataset_tags'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string'>
(text) Dataset ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/<uuid:dataset_id>/tags"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/<uuid:dataset_id>/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/<uuid:dataset_id>/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data":
[
{"id": "4a601f4f-f8a2-4166-ae7c-58c3b252a524",
"name": "123"
},
...
],
"total": 3
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Row>
<Col>

View File

@ -1057,6 +1057,75 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
method='GET'
title='ドキュメントセグメントの詳細を表示'
name='#view_document_segment'
/>
<Row>
<Col>
指定されたナレッジベース内の特定のドキュメントセグメントの詳細を表示します
### パス
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
ナレッジベースID
</Property>
<Property name='document_id' type='string' key='document_id'>
ドキュメントID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
セグメントID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="リクエスト"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="レスポンス">
```json {{ title: 'Response' }}
{
"data": {
"id": "セグメントID",
"position": 2,
"document_id": "ドキュメントID",
"content": "セグメント内容テキスト",
"sign_content": "署名内容テキスト",
"answer": "回答内容(Q&Aモードの場合)",
"word_count": 470,
"tokens": 382,
"keywords": ["キーワード1", "キーワード2"],
"index_node_id": "インデックスードID",
"index_node_hash": "インデックスノードハッシュ",
"hit_count": 0,
"enabled": true,
"status": "completed",
"created_by": "作成者ID",
"created_at": 作成タイムスタンプ,
"updated_at": 更新タイムスタンプ,
"indexing_at": インデックス作成タイムスタンプ,
"completed_at": 完了タイムスタンプ,
"error": null,
"child_chunks": []
},
"doc_form": "text_model"
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
method='DELETE'
title='ドキュメント内のチャンクを削除'
name='#delete_segment'
@ -1100,7 +1169,6 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
method='POST'
title='ドキュメント内のチャンクを更新'
name='#update_segment'
@ -1528,20 +1596,45 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
クエリキーワード
</Property>
<Property name='retrieval_model' type='object' key='retrieval_model'>
検索モデル (オプション、入力されない場合はデフォルトの方法でリコールされます)
- <code>search_method</code> (text) 検索方法: 以下の 4 つのキーワードのいずれかが必要です
- <code>keyword_search</code> キーワード検索
- <code>semantic_search</code> セマンティック検索
- <code>full_text_search</code> 全文検索
- <code>hybrid_search</code> ハイブリッド検索
- <code>reranking_enable</code> (bool) 再ランキングを有効にするかどうか、検索モードが semantic_search または hybrid_search の場合に必須 (オプション)
- <code>reranking_mode</code> (object) 再ランキングモデル構成、再ランキングが有効な場合に必須
- <code>reranking_provider_name</code> (string) 再ランキングモデルプロバイダー
- <code>reranking_model_name</code> (string) 再ランキングモデル名
- <code>weights</code> (float) ハイブリッド検索モードでのセマンティック検索の重み設定
- <code>top_k</code> (integer) 返される結果の数 (オプション)
- <code>score_threshold_enabled</code> (bool) スコア閾値を有効にするかどうか
- <code>score_threshold</code> (float) スコア閾値
検索パラメータ(オプション、入力されない場合はデフォルトの方法でリコールされます)
- <code>search_method</code> (text) 検索方法: 以下の4つのキーワードのいずれかが必要です
- <code>keyword_search</code> キーワード検索
- <code>semantic_search</code> セマンティック検索
- <code>full_text_search</code> 全文検索
- <code>hybrid_search</code> ハイブリッド検索
- <code>reranking_enable</code> (bool) 再ランキングを有効にするかどうか、検索モードがsemantic_searchまたはhybrid_searchの場合に必須オプション
- <code>reranking_mode</code> (object) 再ランキングモデル構成、再ランキングが有効な場合に必須
- <code>reranking_provider_name</code> (string) 再ランキングモデルプロバイダー
- <code>reranking_model_name</code> (string) 再ランキングモデル名
- <code>weights</code> (float) ハイブリッド検索モードでのセマンティック検索の重み設定
- <code>top_k</code> (integer) 返される結果の数(オプション)
- <code>score_threshold_enabled</code> (bool) スコア閾値を有効にするかどうか
- <code>score_threshold</code> (float) スコア閾値
- <code>metadata_filtering_conditions</code> (object) メタデータフィルタリング条件
- <code>logical_operator</code> (string) 論理演算子: <code>and</code> | <code>or</code>
- <code>conditions</code> (array[object]) 条件リスト
- <code>name</code> (string) メタデータフィールド名
- <code>comparison_operator</code> (string) 比較演算子、許可される値:
- 文字列比較:
- <code>contains</code>: 含む
- <code>not contains</code>: 含まない
- <code>start with</code>: で始まる
- <code>end with</code>: で終わる
- <code>is</code>: 等しい
- <code>is not</code>: 等しくない
- <code>empty</code>: 空
- <code>not empty</code>: 空でない
- 数値比較:
- <code>=</code>: 等しい
- <code>≠</code>: 等しくない
- <code>></code>: より大きい
- <code>< </code>: より小さい
- <code>≥</code>: 以上
- <code>≤</code>: 以下
- 時間比較:
- <code>before</code>: より前
- <code>after</code>: より後
- <code>value</code> (string|number|null) 比較値
</Property>
<Property name='external_retrieval_model' type='object' key='external_retrieval_model'>
未使用フィールド
@ -1566,7 +1659,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
"weights": null,
"top_k": 1,
"score_threshold_enabled": false,
"score_threshold": null
"score_threshold": null,
"metadata_filtering_conditions": {
"logical_operator": "and",
"conditions": [
{
"name": "document_name",
"comparison_operator": "contains",
"value": "test"
}
]
}
}
}'`}
>
@ -1898,6 +2001,313 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='POST'
title='ナレッジベースタイプタグの新規作成'
name='#create_new_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='name' type='string'>
(text) 新しいタグ名、必須、最大長50文字
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "testtag1"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"name": "testtag1"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "eddb66c2-04a1-4e3a-8cb2-75abd01e12a6",
"name": "testtag1",
"type": "knowledge",
"binding_count": 0
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='GET'
title='ナレッジベースタイプタグの取得'
name='#get_knowledge_type_tags'
/>
<Row>
<Col>
### Request Body
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/tags"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
[
{
"id": "39d6934c-ed36-463d-b4a7-377fa1503dc0",
"name": "testtag1",
"type": "knowledge",
"binding_count": "0"
},
...
]
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='PATCH'
title='ナレッジベースタイプタグ名の変更'
name='#modify_knowledge_tag_name'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='name' type='string'>
(text) 変更後のタグ名、必須、最大長50文字
</Property>
<Property name='tag_id' type='string'>
(text) タグID、必須
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="PATCH"
label="/datasets/tags"
targetCode={`curl --location --request PATCH '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "testtag2", "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}`}
>
```bash {{ title: 'cURL' }}
curl --location --request PATCH '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"name": "testtag2", "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "eddb66c2-04a1-4e3a-8cb2-75abd01e12a6",
"name": "tag-renamed",
"type": "knowledge",
"binding_count": 0
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='DELETE'
title='ナレッジベースタイプタグの削除'
name='#delete_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_id' type='string'>
(text) タグID、必須
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="DELETE"
label="/datasets/tags"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{ "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}`}
>
```bash {{ title: 'cURL' }}
curl --location --request DELETE '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags/binding'
method='POST'
title='ナレッジベースをナレッジベースタイプタグに紐付け'
name='#bind_dataset_to_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_ids' type='list'>
(list) タグIDリスト、必須
</Property>
<Property name='target_id' type='string'>
(text) ナレッジベースID、必須
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags/binding"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags/binding' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"tag_ids": ["65cc29be-d072-4e26-adf4-2f727644da29","1e5348f3-d3ff-42b8-a1b7-0a86d518001a"], "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags/binding' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_ids": ["65cc29be-d072-4e26-adf4-2f727644da29","1e5348f3-d3ff-42b8-a1b7-0a86d518001a"], "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags/unbinding'
method='POST'
title='ナレッジベースとナレッジベースタイプタグの紐付け解除'
name='#unbind_dataset_and_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_id' type='string'>
(text) タグID、必須
</Property>
<Property name='target_id' type='string'>
(text) ナレッジベースID、必須
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags/unbinding"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags/unbinding' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"tag_id": "1e5348f3-d3ff-42b8-a1b7-0a86d518001a", "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags/unbinding' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_id": "1e5348f3-d3ff-42b8-a1b7-0a86d518001a", "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/<uuid:dataset_id>/tags'
method='POST'
title='ナレッジベースに紐付けられたタグの照会'
name='#query_dataset_tags'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string'>
(text) ナレッジベースID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/<uuid:dataset_id>/tags"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/<uuid:dataset_id>/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/<uuid:dataset_id>/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data":
[
{"id": "4a601f4f-f8a2-4166-ae7c-58c3b252a524",
"name": "123"
},
...
],
"total": 3
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Row>

View File

@ -1351,6 +1351,75 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<Heading
url='/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}'
method='GET'
title='查看文档分段详情'
name='#view_document_segment'
/>
<Row>
<Col>
查看指定知识库中特定文档的分段详情
### Path
<Properties>
<Property name='dataset_id' type='string' key='dataset_id'>
知识库 ID
</Property>
<Property name='document_id' type='string' key='document_id'>
文档 ID
</Property>
<Property name='segment_id' type='string' key='segment_id'>
分段 ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \\\n--header 'Authorization: Bearer {api_key}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}' \
--header 'Authorization: Bearer {api_key}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data": {
"id": "分段唯一ID",
"position": 2,
"document_id": "所属文档ID",
"content": "分段内容文本",
"sign_content": "签名内容文本",
"answer": "答案内容(如果有)",
"word_count": 470,
"tokens": 382,
"keywords": ["关键词1", "关键词2"],
"index_node_id": "索引节点ID",
"index_node_hash": "索引节点哈希值",
"hit_count": 0,
"enabled": true,
"status": "completed",
"created_by": "创建者ID",
"created_at": 创建时间戳,
"updated_at": 更新时间戳,
"indexing_at": 索引时间戳,
"completed_at": 完成时间戳,
"error": null,
"child_chunks": []
},
"doc_form": "text_model"
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
method='POST'
title='更新文档分段'
name='#update_segment'
@ -1827,6 +1896,31 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
- <code>top_k</code> (integer) 返回结果数量,非必填
- <code>score_threshold_enabled</code> (bool) 是否开启 score 阈值
- <code>score_threshold</code> (float) Score 阈值
- <code>metadata_filtering_conditions</code> (object) 元数据过滤条件
- <code>logical_operator</code> (string) 逻辑运算符: <code>and</code> | <code>or</code>
- <code>conditions</code> (array[object]) 条件列表
- <code>name</code> (string) 元数据字段名
- <code>comparison_operator</code> (string) 比较运算符,可选值:
- 字符串比较:
- <code>contains</code>: 包含
- <code>not contains</code>: 不包含
- <code>start with</code>: 以...开头
- <code>end with</code>: 以...结尾
- <code>is</code>: 等于
- <code>is not</code>: 不等于
- <code>empty</code>: 为空
- <code>not empty</code>: 不为空
- 数值比较:
- <code>=</code>: 等于
- <code>≠</code>: 不等于
- <code>></code>: 大于
- <code> < </code>: 小于
- <code>≥</code>: 大于等于
- <code>≤</code>: 小于等于
- 时间比较:
- <code>before</code>: 早于
- <code>after</code>: 晚于
- <code>value</code> (string|number|null) 比较值
</Property>
<Property name='external_retrieval_model' type='object' key='external_retrieval_model'>
未启用字段
@ -1851,7 +1945,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
"weights": null,
"top_k": 1,
"score_threshold_enabled": false,
"score_threshold": null
"score_threshold": null,
"metadata_filtering_conditions": {
"logical_operator": "and",
"conditions": [
{
"name": "document_name",
"comparison_operator": "contains",
"value": "test"
}
]
}
}
}'`}
>
@ -2287,6 +2391,314 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='POST'
title='新增知识库类型标签'
name='#create_new_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='name' type='string'>
(text) 新标签名称必填最大长度为50
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "testtag1"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"name": "testtag1"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "eddb66c2-04a1-4e3a-8cb2-75abd01e12a6",
"name": "testtag1",
"type": "knowledge",
"binding_count": 0
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='GET'
title='获取知识库类型标签'
name='#get_knowledge_type_tags'
/>
<Row>
<Col>
### Request Body
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="GET"
label="/datasets/tags"
targetCode={`curl --location --request GET '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json'`}
>
```bash {{ title: 'cURL' }}
curl --location --request GET '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
[
{
"id": "39d6934c-ed36-463d-b4a7-377fa1503dc0",
"name": "testtag1",
"type": "knowledge",
"binding_count": "0"
},
...
]
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='PATCH'
title='修改知识库类型标签名称'
name='#modify_knowledge_tag_name'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='name' type='string'>
(text) 修改后的标签名称必填最大长度为50
</Property>
<Property name='tag_id' type='string'>
(text) 标签ID必填
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="PATCH"
label="/datasets/tags"
targetCode={`curl --location --request PATCH '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"name": "testtag2", "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}`}
>
```bash {{ title: 'cURL' }}
curl --location --request PATCH '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"name": "testtag2", "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"id": "eddb66c2-04a1-4e3a-8cb2-75abd01e12a6",
"name": "tag-renamed",
"type": "knowledge",
"binding_count": 0
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags'
method='DELETE'
title='删除知识库类型标签'
name='#delete_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_id' type='string'>
(text) 标签ID必填
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="DELETE"
label="/datasets/tags"
targetCode={`curl --location --request DELETE '${props.apiBaseUrl}/datasets/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{ "tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}`}
>
```bash {{ title: 'cURL' }}
curl --location --request DELETE '${props.apiBaseUrl}/datasets/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_id": "e1a0a3db-ee34-4e04-842a-81555d5316fd"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags/binding'
method='POST'
title='绑定知识库到知识库类型标签'
name='#bind_dataset_to_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_ids' type='list'>
(list) 标签ID列表必填
</Property>
<Property name='target_id' type='string'>
(text) 知识库ID必填
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags/binding"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags/binding' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"tag_ids": ["65cc29be-d072-4e26-adf4-2f727644da29","1e5348f3-d3ff-42b8-a1b7-0a86d518001a"], "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags/binding' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_ids": ["65cc29be-d072-4e26-adf4-2f727644da29","1e5348f3-d3ff-42b8-a1b7-0a86d518001a"], "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/tags/unbinding'
method='POST'
title='解绑知识库和知识库类型标签'
name='#unbind_dataset_and_knowledge_tag'
/>
<Row>
<Col>
### Request Body
<Properties>
<Property name='tag_id' type='string'>
(text) 标签ID必填
</Property>
<Property name='target_id' type='string'>
(text) 知识库ID必填
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/tags/unbinding"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/tags/unbinding' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{"tag_id": "1e5348f3-d3ff-42b8-a1b7-0a86d518001a", "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/tags/unbinding' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
--data-raw '{"tag_id": "1e5348f3-d3ff-42b8-a1b7-0a86d518001a", "target_id": "a932ea9f-fae1-4b2c-9b65-71c56e2cacd6"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{"result": "success"}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Heading
url='/datasets/<uuid:dataset_id>/tags'
method='POST'
title='查询知识库已绑定的标签'
name='#query_dataset_tags'
/>
<Row>
<Col>
### Path
<Properties>
<Property name='dataset_id' type='string'>
(text) 知识库ID
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup
title="Request"
tag="POST"
label="/datasets/<uuid:dataset_id>/tags"
targetCode={`curl --location --request POST '${props.apiBaseUrl}/datasets/<uuid:dataset_id>/tags' \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n`}
>
```bash {{ title: 'cURL' }}
curl --location --request POST '${props.apiBaseUrl}/datasets/<uuid:dataset_id>/tags' \
--header 'Authorization: Bearer {api_key}' \
--header 'Content-Type: application/json' \
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: 'Response' }}
{
"data":
[
{"id": "4a601f4f-f8a2-4166-ae7c-58c3b252a524",
"name": "123"
},
...
],
"total": 3
}
```
</CodeGroup>
</Col>
</Row>
<hr className='ml-0 mr-0' />
<Row>

View File

@ -6,10 +6,12 @@ import Button from '../components/base/button'
import Avatar from './avatar'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { useCallback } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Header = () => {
const { t } = useTranslation()
const router = useRouter()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const back = useCallback(() => {
router.back()
@ -19,7 +21,13 @@ const Header = () => {
<div className='flex flex-1 items-center justify-between px-4'>
<div className='flex items-center gap-3'>
<div className='flex cursor-pointer items-center' onClick={back}>
<DifyLogo />
{systemFeatures.branding.enabled && systemFeatures.branding.login_page_logo
? <img
src={systemFeatures.branding.login_page_logo}
className='block h-[22px] w-auto object-contain'
alt='Dify logo'
/>
: <DifyLogo />}
</div>
<div className='h-4 w-[1px] origin-center rotate-[11.31deg] bg-divider-regular' />
<p className='title-3xl-semi-bold relative mt-[-2px] text-text-primary'>{t('common.account.account')}</p>

View File

@ -1,6 +1,6 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import copy from 'copy-to-clipboard'
@ -32,6 +32,7 @@ import cn from '@/utils/classnames'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import { canFindTool } from '@/utils'
import { useMittContextSelector } from '@/context/mitt-context'
type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null
const AgentTools: FC = () => {
@ -39,7 +40,6 @@ const AgentTools: FC = () => {
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext)
const formattingChangedDispatcher = useFormattingChangedDispatcher()
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
const currentCollection = useMemo(() => {
if (!currentTool) return null
@ -61,6 +61,17 @@ const AgentTools: FC = () => {
collection,
}
})
const useSubscribe = useMittContextSelector(s => s.useSubscribe)
const handleUpdateToolsWhenInstallToolSuccess = useCallback((installedPluginNames: string[]) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.forEach((item: any) => {
if (item.isDeleted && installedPluginNames.includes(item.provider_id))
item.isDeleted = false
})
})
setModelConfig(newModelConfig)
}, [modelConfig, setModelConfig])
useSubscribe('plugin:install:success', handleUpdateToolsWhenInstallToolSuccess as any)
const handleToolSettingChange = (value: Record<string, any>) => {
const newModelConfig = produce(modelConfig, (draft) => {
@ -132,7 +143,7 @@ const AgentTools: FC = () => {
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
selectedTools={tools}
selectedTools={tools as any}
/>
</>
)}

View File

@ -99,7 +99,15 @@ const DebugWithMultipleModel = () => {
}, [twoLine, threeLine, fourLine])
const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
const inputsForm = modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
const inputsForm = modelConfig.configs.prompt_variables
.filter(item => item.type !== 'api')
.map(item => ({
...item,
label: item.name,
variable: item.key,
hide: item.hide ?? false,
required: item.required ?? false,
})) as InputForm[]
return (
<div className='flex h-full flex-col'>
@ -133,6 +141,7 @@ const DebugWithMultipleModel = () => {
{isChatMode && (
<div className='shrink-0 px-6 pb-0'>
<ChatInputArea
botName='Bot'
showFeatureBar
showFileUpload={false}
onFeatureBarClick={setShowAppConfigureFeaturesModal}

View File

@ -79,6 +79,7 @@ import {
} from '@/utils'
import PluginDependency from '@/app/components/workflow/plugin-dependency'
import { supportFunctionCall } from '@/utils/tool-call'
import { MittProvider } from '@/context/mitt-context'
type PublishConfig = {
modelConfig: ModelConfig
@ -908,7 +909,7 @@ const Configuration: FC = () => {
}}
>
<FeaturesProvider features={featuresData}>
<>
<MittProvider>
<div className="flex h-full flex-col">
<div className='relative flex h-[200px] grow pt-14'>
{/* Header */}
@ -1060,7 +1061,7 @@ const Configuration: FC = () => {
/>
)}
<PluginDependency />
</>
</MittProvider>
</FeaturesProvider>
</ConfigContext.Provider>
)

View File

@ -32,7 +32,6 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import TextGeneration from '@/app/components/app/text-generate/item'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import MessageLogModal from '@/app/components/base/message-log-modal'
import PromptLogModal from '@/app/components/base/prompt-log-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp'
@ -191,13 +190,11 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const { userProfile: { timezone } } = useAppContext()
const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem,
setCurrentLogItem: state.setCurrentLogItem,
showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal,
showPromptLogModal: state.showPromptLogModal,
setShowPromptLogModal: state.setShowPromptLogModal,
currentLogModalActiveTab: state.currentLogModalActiveTab,
})))
const { t } = useTranslation()
@ -518,16 +515,6 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
defaultTab={currentLogModalActiveTab}
/>
)}
{showPromptLogModal && (
<PromptLogModal
width={width}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowPromptLogModal(false)
}}
/>
)}
</div>
)
}

View File

@ -146,10 +146,12 @@ const Sidebar = ({ isPanel }: Props) => {
'flex shrink-0 items-center gap-1.5 px-1',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
{systemFeatures.branding.enabled ? (
<img src={systemFeatures.branding.login_page_logo} alt='logo' className='block h-5 w-auto' />
) : (
<DifyLogo size='small' />)
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt='logo' className='block h-5 w-auto' />
: appData?.custom_config?.replace_webapp_logo
? <img src={`${appData?.custom_config?.replace_webapp_logo}`} alt='logo' className='block h-5 w-auto' />
: <DifyLogo size='small' />
}
</div>
)}

View File

@ -234,6 +234,4 @@ const Answer: FC<AnswerProps> = ({
)
}
export default memo(Answer, (prevProps, nextProps) =>
prevProps.responding === false && nextProps.responding === false,
)
export default memo(Answer)

View File

@ -29,6 +29,7 @@ import type { FileUpload } from '@/app/components/base/features/types'
import { TransferMethod } from '@/types/app'
type ChatInputAreaProps = {
botName?: string
showFeatureBar?: boolean
showFileUpload?: boolean
featureBarDisabled?: boolean
@ -43,6 +44,7 @@ type ChatInputAreaProps = {
disabled?: boolean
}
const ChatInputArea = ({
botName,
showFeatureBar,
showFileUpload,
featureBarDisabled,
@ -192,7 +194,7 @@ const ChatInputArea = ({
className={cn(
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-tertiary outline-none',
)}
placeholder={t('common.chat.inputPlaceholder') || ''}
placeholder={t('common.chat.inputPlaceholder', { botName }) || ''}
autoFocus
minRows={1}
onResize={handleTextareaResize}

View File

@ -366,8 +366,9 @@ export const useChat = (
if (!newResponseItem)
return
const isUseAgentThought = newResponseItem.agent_thoughts?.length > 0
updateChatTreeNode(responseItem.id, {
content: newResponseItem.answer,
content: isUseAgentThought ? '' : newResponseItem.answer,
log: [
...newResponseItem.message,
...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'

View File

@ -303,6 +303,7 @@ const Chat: FC<ChatProps> = ({
{
!noChatInput && (
<ChatInputArea
botName={appData?.site.title || ''}
disabled={inputDisabled}
showFeatureBar={showFeatureBar}
showFileUpload={showFileUpload}

View File

@ -117,7 +117,7 @@ const Question: FC<QuestionProps> = ({
</div>
<div
ref={contentRef}
className='w-full rounded-2xl bg-[#D1E9FF]/50 px-4 py-3 text-sm text-gray-900'
className='w-full rounded-2xl bg-background-gradient-bg-fill-chat-bubble-bg-3 px-4 py-3 text-sm text-text-primary'
style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}}
>
{

View File

@ -13,6 +13,7 @@ import Divider from '@/app/components/base/divider'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IHeaderProps = {
isMobile?: boolean
@ -42,6 +43,7 @@ const Header: FC<IHeaderProps> = ({
const [parentOrigin, setParentOrigin] = useState('')
const [showToggleExpandButton, setShowToggleExpandButton] = useState(false)
const [expanded, setExpanded] = useState(false)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const handleMessageReceived = useCallback((event: MessageEvent) => {
let currentParentOrigin = parentOrigin
@ -85,12 +87,13 @@ const Header: FC<IHeaderProps> = ({
'flex shrink-0 items-center gap-1.5 px-2',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
{appData?.custom_config?.replace_webapp_logo && (
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block h-5 w-auto' />
)}
{!appData?.custom_config?.replace_webapp_logo && (
<DifyLogo size='small' />
)}
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt='logo' className='block h-5 w-auto' />
: appData?.custom_config?.replace_webapp_logo
? <img src={`${appData?.custom_config?.replace_webapp_logo}`} alt='logo' className='block h-5 w-auto' />
: <DifyLogo size='small' />
}
</div>
)}
</div>

View File

@ -22,6 +22,7 @@ import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrappe
import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Chatbot = () => {
const {
@ -37,6 +38,7 @@ const Chatbot = () => {
themeBuilder,
} = useEmbeddedChatbotContext()
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const customConfig = appData?.custom_config
const site = appData?.site
@ -115,12 +117,13 @@ const Chatbot = () => {
'flex shrink-0 items-center gap-1.5 px-2',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
{appData?.custom_config?.replace_webapp_logo && (
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block h-5 w-auto' />
)}
{!appData?.custom_config?.replace_webapp_logo && (
<DifyLogo size='small' />
)}
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt='logo' className='block h-5 w-auto' />
: appData?.custom_config?.replace_webapp_logo
? <img src={`${appData?.custom_config?.replace_webapp_logo}`} alt='logo' className='block h-5 w-auto' />
: <DifyLogo size='small' />
}
</div>
)}
</div>

View File

@ -12,8 +12,7 @@ export class Theme {
public colorPathOnHeader = 'text-text-primary-on-surface'
public backgroundButtonDefaultColorStyle = 'backgroundColor: #1C64F2'
public roundedBackgroundColorStyle = 'backgroundColor: rgb(245 248 255)'
public chatBubbleColorStyle = 'backgroundColor: rgb(225 239 254)'
public chatBubbleColor = 'rgb(225 239 254)'
public chatBubbleColorStyle = ''
constructor(chatColorTheme: string | null = null, chatColorThemeInverted = false) {
this.chatColorTheme = chatColorTheme
@ -29,7 +28,6 @@ export class Theme {
this.backgroundButtonDefaultColorStyle = `backgroundColor: ${this.primaryColor}; color: ${this.colorFontOnHeaderStyle};`
this.roundedBackgroundColorStyle = `backgroundColor: ${hexToRGBA(this.primaryColor, 0.05)}`
this.chatBubbleColorStyle = `backgroundColor: ${hexToRGBA(this.primaryColor, 0.15)}`
this.chatBubbleColor = `${hexToRGBA(this.primaryColor, 0.15)}`
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './OpenaiTeal.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiTeal'
export default Icon

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,20 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './OpenaiYellow.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconData } from '@/app/components/base/icons/IconBase'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiYellow'
export default Icon

View File

@ -28,9 +28,11 @@ export { default as Microsoft } from './Microsoft'
export { default as OpenaiBlack } from './OpenaiBlack'
export { default as OpenaiBlue } from './OpenaiBlue'
export { default as OpenaiGreen } from './OpenaiGreen'
export { default as OpenaiTeal } from './OpenaiTeal'
export { default as OpenaiText } from './OpenaiText'
export { default as OpenaiTransparent } from './OpenaiTransparent'
export { default as OpenaiViolet } from './OpenaiViolet'
export { default as OpenaiYellow } from './OpenaiYellow'
export { default as OpenllmText } from './OpenllmText'
export { default as Openllm } from './Openllm'
export { default as ReplicateText } from './ReplicateText'

View File

@ -3,7 +3,6 @@ import type { FC } from 'react'
import classNames from '@/utils/classnames'
import useTheme from '@/hooks/use-theme'
import { basePath } from '@/utils/var'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type LogoStyle = 'default' | 'monochromeWhite'
export const logoPathMap: Record<LogoStyle, string> = {
@ -32,18 +31,12 @@ const DifyLogo: FC<DifyLogoProps> = ({
}) => {
const { theme } = useTheme()
const themedStyle = (theme === 'dark' && style === 'default') ? 'monochromeWhite' : style
const { systemFeatures } = useGlobalPublicStore()
const hasBrandingLogo = Boolean(systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo)
let src = `${basePath}${logoPathMap[themedStyle]}`
if (hasBrandingLogo)
src = systemFeatures.branding.workspace_logo
return (
<img
src={src}
className={classNames('block object-contain', logoSizeMap[size], hasBrandingLogo && 'w-auto', className)}
alt={hasBrandingLogo ? 'Logo' : 'Dify logo'}
src={`${basePath}${logoPathMap[themedStyle]}`}
className={classNames('block object-contain', logoSizeMap[size], className)}
alt='Dify logo'
/>
)
}

View File

@ -0,0 +1,21 @@
/**
* @fileoverview AudioBlock component for rendering audio elements in Markdown.
* Extracted from the main markdown renderer for modularity.
* Uses the AudioGallery component to display audio players.
*/
import React, { memo } from 'react'
import AudioGallery from '@/app/components/base/audio-gallery'
const AudioBlock: any = memo(({ node }: any) => {
const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src)
if (srcs.length === 0) {
const src = node.properties?.src
if (src)
return <AudioGallery key={src} srcs={[src]} />
return null
}
return <AudioGallery key={srcs.join()} srcs={srcs} />
})
AudioBlock.displayName = 'AudioBlock'
export default AudioBlock

View File

@ -1,34 +1,19 @@
import ReactMarkdown from 'react-markdown'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import ReactEcharts from 'echarts-for-react'
import 'katex/dist/katex.min.css'
import RemarkMath from 'remark-math'
import RemarkBreaks from 'remark-breaks'
import RehypeKatex from 'rehype-katex'
import RemarkGfm from 'remark-gfm'
import RehypeRaw from 'rehype-raw'
import SyntaxHighlighter from 'react-syntax-highlighter'
import {
atelierHeathDark,
atelierHeathLight,
} from 'react-syntax-highlighter/dist/esm/styles/hljs'
import { Component, memo, useEffect, useMemo, useRef, useState } from 'react'
import { flow } from 'lodash-es'
import ActionButton from '@/app/components/base/action-button'
import CopyIcon from '@/app/components/base/copy-icon'
import SVGBtn from '@/app/components/base/svg'
import Flowchart from '@/app/components/base/mermaid'
import ImageGallery from '@/app/components/base/image-gallery'
import { useChatContext } from '@/app/components/base/chat/chat/context'
import VideoGallery from '@/app/components/base/video-gallery'
import AudioGallery from '@/app/components/base/audio-gallery'
import MarkdownButton from '@/app/components/base/markdown-blocks/button'
import MarkdownForm from '@/app/components/base/markdown-blocks/form'
import MarkdownMusic from '@/app/components/base/markdown-blocks/music'
import ThinkBlock from '@/app/components/base/markdown-blocks/think-block'
import { Theme } from '@/types/app'
import useTheme from '@/hooks/use-theme'
import cn from '@/utils/classnames'
import SVGRenderer from './svg-gallery'
import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory
import MarkdownMusic from '@/app/components/base/markdown-blocks/music'
import ErrorBoundary from '@/app/components/base/markdown/error-boundary'
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
const capitalizationLanguageNameMap: Record<string, string> = {
@ -64,50 +49,6 @@ const getCorrectCapitalizationLanguageName = (language: string) => {
return language.charAt(0).toUpperCase() + language.substring(1)
}
const preprocessLaTeX = (content: string) => {
if (typeof content !== 'string')
return content
const codeBlockRegex = /```[\s\S]*?```/g
const codeBlocks = content.match(codeBlockRegex) || []
let processedContent = content.replace(codeBlockRegex, 'CODE_BLOCK_PLACEHOLDER')
processedContent = flow([
(str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/\\\[([\s\S]*?)\\\]/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`),
])(processedContent)
codeBlocks.forEach((block) => {
processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', block)
})
return processedContent
}
const preprocessThinkTag = (content: string) => {
const thinkOpenTagRegex = /<think>\n/g
const thinkCloseTagRegex = /\n<\/think>/g
return flow([
(str: string) => str.replace(thinkOpenTagRegex, '<details data-think=true>\n'),
(str: string) => str.replace(thinkCloseTagRegex, '\n[ENDTHINKFLAG]</details>'),
])(content)
}
export function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null)
return (
<pre ref={ref}>
<span
className="copy-code-button"
></span>
{props.children}
</pre>
)
}
// **Add code block
// Avoid error #185 (Maximum update depth exceeded.
// This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
@ -445,150 +386,4 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
})
CodeBlock.displayName = 'CodeBlock'
const VideoBlock: any = memo(({ node }: any) => {
const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src)
if (srcs.length === 0) {
const src = node.properties?.src
if (src)
return <VideoGallery key={src} srcs={[src]} />
return null
}
return <VideoGallery key={srcs.join()} srcs={srcs} />
})
VideoBlock.displayName = 'VideoBlock'
const AudioBlock: any = memo(({ node }: any) => {
const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src)
if (srcs.length === 0) {
const src = node.properties?.src
if (src)
return <AudioGallery key={src} srcs={[src]} />
return null
}
return <AudioGallery key={srcs.join()} srcs={srcs} />
})
AudioBlock.displayName = 'AudioBlock'
const ScriptBlock = memo(({ node }: any) => {
const scriptContent = node.children[0]?.value || ''
return `<script>${scriptContent}</script>`
})
ScriptBlock.displayName = 'ScriptBlock'
const Paragraph = (paragraph: any) => {
const { node }: any = paragraph
const children_node = node.children
if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') {
return (
<div className="markdown-img-wrapper">
<ImageGallery srcs={[children_node[0].properties.src]} />
{
Array.isArray(paragraph.children) && paragraph.children.length > 1 && (
<div className="mt-2">{paragraph.children.slice(1)}</div>
)
}
</div>
)
}
return <p>{paragraph.children}</p>
}
const Img = ({ src }: any) => {
return <div className="markdown-img-wrapper"><ImageGallery srcs={[src]} /></div>
}
const Link = ({ node, children, ...props }: any) => {
if (node.properties?.href && node.properties.href?.toString().startsWith('abbr')) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { onSend } = useChatContext()
const hidden_text = decodeURIComponent(node.properties.href.toString().split('abbr:')[1])
return <abbr className="cursor-pointer underline !decoration-primary-700 decoration-dashed" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr>
}
else {
return <a {...props} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || 'Download'}</a>
}
}
export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) {
const latexContent = flow([
preprocessThinkTag,
preprocessLaTeX,
])(props.content)
return (
<div className={cn('markdown-body', '!text-text-primary', props.className)}>
<ReactMarkdown
remarkPlugins={[
RemarkGfm,
[RemarkMath, { singleDollarTextMath: false }],
RemarkBreaks,
]}
rehypePlugins={[
RehypeKatex,
RehypeRaw as any,
// The Rehype plug-in is used to remove the ref attribute of an element
() => {
return (tree) => {
const iterate = (node: any) => {
if (node.type === 'element' && node.properties?.ref)
delete node.properties.ref
if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) {
node.type = 'text'
node.value = `<${node.tagName}`
}
if (node.children)
node.children.forEach(iterate)
}
tree.children.forEach(iterate)
}
},
]}
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
components={{
code: CodeBlock,
img: Img,
video: VideoBlock,
audio: AudioBlock,
a: Link,
p: Paragraph,
button: MarkdownButton,
form: MarkdownForm,
script: ScriptBlock as any,
details: ThinkBlock,
}}
>
{/* Markdown detect has problem. */}
{latexContent}
</ReactMarkdown>
</div>
)
}
// **Add an ECharts runtime error handler
// Avoid error #7832 (Crash when ECharts accesses undefined objects)
// This can happen when a component attempts to access an undefined object that references an unregistered map, causing the program to crash.
export default class ErrorBoundary extends Component {
constructor(props: any) {
super(props)
this.state = { hasError: false }
}
componentDidCatch(error: any, errorInfo: any) {
this.setState({ hasError: true })
console.error(error, errorInfo)
}
render() {
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
if (this.state.hasError)
return <div>Oops! An error occurred. This could be due to an ECharts runtime error or invalid SVG content. <br />(see the browser console for more information)</div>
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
return this.props.children
}
}
export default CodeBlock

View File

@ -0,0 +1,13 @@
/**
* @fileoverview Img component for rendering <img> tags in Markdown.
* Extracted from the main markdown renderer for modularity.
* Uses the ImageGallery component to display images.
*/
import React from 'react'
import ImageGallery from '@/app/components/base/image-gallery'
const Img = ({ src }: any) => {
return <div className="markdown-img-wrapper"><ImageGallery srcs={[src]} /></div>
}
export default Img

View File

@ -0,0 +1,18 @@
/**
* @fileoverview Barrel file for all markdown block components.
* This allows for cleaner imports in other parts of the application.
*/
export { default as AudioBlock } from './audio-block'
export { default as CodeBlock } from './code-block'
export { default as Img } from './img'
export { default as Link } from './link'
export { default as Paragraph } from './paragraph'
export { default as PreCode } from './pre-code'
export { default as ScriptBlock } from './script-block'
export { default as VideoBlock } from './video-block'
// Assuming these are also standalone components in this directory intended for Markdown rendering
export { default as MarkdownButton } from './button'
export { default as MarkdownForm } from './form'
export { default as ThinkBlock } from './think-block'

View File

@ -0,0 +1,21 @@
/**
* @fileoverview Link component for rendering <a> tags in Markdown.
* Extracted from the main markdown renderer for modularity.
* Handles special rendering for "abbr:" type links for interactive chat actions.
*/
import React from 'react'
import { useChatContext } from '@/app/components/base/chat/chat/context'
const Link = ({ node, children, ...props }: any) => {
const { onSend } = useChatContext()
if (node.properties?.href && node.properties.href?.toString().startsWith('abbr')) {
const hidden_text = decodeURIComponent(node.properties.href.toString().split('abbr:')[1])
return <abbr className="cursor-pointer underline !decoration-primary-700 decoration-dashed" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value || ''}>{node.children[0]?.value || ''}</abbr>
}
else {
return <a {...props} target="_blank" className="cursor-pointer underline !decoration-primary-700 decoration-dashed">{children || 'Download'}</a>
}
}
export default Link

View File

@ -0,0 +1,27 @@
/**
* @fileoverview Paragraph component for rendering <p> tags in Markdown.
* Extracted from the main markdown renderer for modularity.
* Handles special rendering for paragraphs that directly contain an image.
*/
import React from 'react'
import ImageGallery from '@/app/components/base/image-gallery'
const Paragraph = (paragraph: any) => {
const { node }: any = paragraph
const children_node = node.children
if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') {
return (
<div className="markdown-img-wrapper">
<ImageGallery srcs={[children_node[0].properties.src]} />
{
Array.isArray(paragraph.children) && paragraph.children.length > 1 && (
<div className="mt-2">{paragraph.children.slice(1)}</div>
)
}
</div>
)
}
return <p>{paragraph.children}</p>
}
export default Paragraph

View File

@ -0,0 +1,21 @@
/**
* @fileoverview PreCode component for rendering <pre> tags in Markdown.
* Extracted from the main markdown renderer for modularity.
* This is a simple wrapper around the HTML <pre> element.
*/
import React, { useRef } from 'react'
function PreCode(props: { children: any }) {
const ref = useRef<HTMLPreElement>(null)
return (
<pre ref={ref}>
<span
className="copy-code-button"
></span>
{props.children}
</pre>
)
}
export default PreCode

View File

@ -0,0 +1,15 @@
/**
* @fileoverview ScriptBlock component for handling <script> tags in Markdown.
* Extracted from the main markdown renderer for modularity.
* Note: Current implementation returns the script tag as a string, which might not execute as expected in React.
* This behavior is preserved from the original implementation and may need review for security and functionality.
*/
import { memo } from 'react'
const ScriptBlock = memo(({ node }: any) => {
const scriptContent = node.children[0]?.value || ''
return `<script>${scriptContent}</script>`
})
ScriptBlock.displayName = 'ScriptBlock'
export default ScriptBlock

View File

@ -0,0 +1,21 @@
/**
* @fileoverview VideoBlock component for rendering video elements in Markdown.
* Extracted from the main markdown renderer for modularity.
* Uses the VideoGallery component to display videos.
*/
import React, { memo } from 'react'
import VideoGallery from '@/app/components/base/video-gallery'
const VideoBlock: any = memo(({ node }: any) => {
const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src)
if (srcs.length === 0) {
const src = node.properties?.src
if (src)
return <VideoGallery key={src} srcs={[src]} />
return null
}
return <VideoGallery key={srcs.join()} srcs={srcs} />
})
VideoBlock.displayName = 'VideoBlock'
export default VideoBlock

View File

@ -0,0 +1,33 @@
/**
* @fileoverview ErrorBoundary component for React.
* This component was extracted from the main markdown renderer.
* It catches JavaScript errors anywhere in its child component tree,
* logs those errors, and displays a fallback UI instead of the crashed component tree.
* Primarily used around complex rendering logic like ECharts or SVG within Markdown.
*/
import React, { Component } from 'react'
// **Add an ECharts runtime error handler
// Avoid error #7832 (Crash when ECharts accesses undefined objects)
// This can happen when a component attempts to access an undefined object that references an unregistered map, causing the program to crash.
export default class ErrorBoundary extends Component {
constructor(props: any) {
super(props)
this.state = { hasError: false }
}
componentDidCatch(error: any, errorInfo: any) {
this.setState({ hasError: true })
console.error(error, errorInfo)
}
render() {
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
if (this.state.hasError)
return <div>Oops! An error occurred. This could be due to an ECharts runtime error or invalid SVG content. <br />(see the browser console for more information)</div>
// eslint-disable-next-line ts/ban-ts-comment
// @ts-expect-error
return this.props.children
}
}

View File

@ -0,0 +1,87 @@
import ReactMarkdown from 'react-markdown'
import 'katex/dist/katex.min.css'
import RemarkMath from 'remark-math'
import RemarkBreaks from 'remark-breaks'
import RehypeKatex from 'rehype-katex'
import RemarkGfm from 'remark-gfm'
import RehypeRaw from 'rehype-raw'
import { flow } from 'lodash-es'
import cn from '@/utils/classnames'
import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils'
import {
AudioBlock,
CodeBlock,
Img,
Link,
MarkdownButton,
MarkdownForm,
Paragraph,
ScriptBlock,
ThinkBlock,
VideoBlock,
} from '@/app/components/base/markdown-blocks'
/**
* @fileoverview Main Markdown rendering component.
* This file was refactored to extract individual block renderers and utility functions
* into separate modules for better organization and maintainability as of [Date of refactor].
* Further refactoring candidates (custom block components not fitting general categories)
* are noted in their respective files if applicable.
*/
export function Markdown(props: { content: string; className?: string; customDisallowedElements?: string[] }) {
const latexContent = flow([
preprocessThinkTag,
preprocessLaTeX,
])(props.content)
return (
<div className={cn('markdown-body', '!text-text-primary', props.className)}>
<ReactMarkdown
remarkPlugins={[
RemarkGfm,
[RemarkMath, { singleDollarTextMath: false }],
RemarkBreaks,
]}
rehypePlugins={[
RehypeKatex,
RehypeRaw as any,
// The Rehype plug-in is used to remove the ref attribute of an element
() => {
return (tree: any) => {
const iterate = (node: any) => {
if (node.type === 'element' && node.properties?.ref)
delete node.properties.ref
if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) {
node.type = 'text'
node.value = `<${node.tagName}`
}
if (node.children)
node.children.forEach(iterate)
}
tree.children.forEach(iterate)
}
},
]}
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
components={{
code: CodeBlock,
img: Img,
video: VideoBlock,
audio: AudioBlock,
a: Link,
p: Paragraph,
button: MarkdownButton,
form: MarkdownForm,
script: ScriptBlock as any,
details: ThinkBlock,
}}
>
{/* Markdown detect has problem. */}
{latexContent}
</ReactMarkdown>
</div>
)
}

View File

@ -0,0 +1,38 @@
/**
* @fileoverview Utility functions for preprocessing Markdown content.
* These functions were extracted from the main markdown renderer for better separation of concerns.
* Includes preprocessing for LaTeX and custom "think" tags.
*/
import { flow } from 'lodash-es'
export const preprocessLaTeX = (content: string) => {
if (typeof content !== 'string')
return content
const codeBlockRegex = /```[\s\S]*?```/g
const codeBlocks = content.match(codeBlockRegex) || []
let processedContent = content.replace(codeBlockRegex, 'CODE_BLOCK_PLACEHOLDER')
processedContent = flow([
(str: string) => str.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/\\\[([\s\S]*?)\\\]/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/\\\((.*?)\\\)/g, (_, equation) => `$$${equation}$$`),
(str: string) => str.replace(/(^|[^\\])\$(.+?)\$/g, (_, prefix, equation) => `${prefix}$${equation}$`),
])(processedContent)
codeBlocks.forEach((block) => {
processedContent = processedContent.replace('CODE_BLOCK_PLACEHOLDER', block)
})
return processedContent
}
export const preprocessThinkTag = (content: string) => {
const thinkOpenTagRegex = /<think>\n/g
const thinkCloseTagRegex = /\n<\/think>/g
return flow([
(str: string) => str.replace(thinkOpenTagRegex, '<details data-think=true>\n'),
(str: string) => str.replace(thinkCloseTagRegex, '\n[ENDTHINKFLAG]</details>'),
(str: string) => str.replace(/(<\/details>)(?![^\S\r\n]*[\r\n])(?![^\S\r\n]*$)/g, '$1\n'),
])(content)
}

View File

@ -91,6 +91,11 @@ const initMermaid = () => {
numberSectionStyles: 4,
axisFormat: '%Y-%m-%d',
},
mindmap: {
useMaxWidth: true,
padding: 10,
diagramPadding: 20,
},
maxTextSize: 50000,
})
isMermaidInitialized = true
@ -289,11 +294,12 @@ const Flowchart = React.forwardRef((props: {
try {
let finalCode: string
// Check if it's a gantt chart
// Check if it's a gantt chart or mindmap
const isGanttChart = primitiveCode.trim().startsWith('gantt')
const isMindMap = primitiveCode.trim().startsWith('mindmap')
if (isGanttChart) {
// For gantt charts, ensure each task is on its own line
if (isGanttChart || isMindMap) {
// For gantt charts and mindmaps, ensure each task is on its own line
// and preserve exact whitespace/format
finalCode = primitiveCode.trim()
}
@ -352,6 +358,11 @@ const Flowchart = React.forwardRef((props: {
numberSectionStyles: 4,
axisFormat: '%Y-%m-%d',
},
mindmap: {
useMaxWidth: true,
padding: 10,
diagramPadding: 20,
},
}
if (look === 'classic') {
@ -476,15 +487,15 @@ const Flowchart = React.forwardRef((props: {
'bg-white': currentTheme === Theme.light,
'bg-slate-900': currentTheme === Theme.dark,
}),
mermaidDiv: cn('mermaid relative h-auto w-full cursor-pointer', {
mermaidDiv: cn('mermaid cursor-pointer h-auto w-full relative', {
'bg-white': currentTheme === Theme.light,
'bg-slate-900': currentTheme === Theme.dark,
}),
errorMessage: cn('px-[26px] py-4', {
errorMessage: cn('py-4 px-[26px]', {
'text-red-500': currentTheme === Theme.light,
'text-red-400': currentTheme === Theme.dark,
}),
errorIcon: cn('h-6 w-6', {
errorIcon: cn('w-6 h-6', {
'text-red-500': currentTheme === Theme.light,
'text-red-400': currentTheme === Theme.dark,
}),
@ -492,7 +503,7 @@ const Flowchart = React.forwardRef((props: {
'text-gray-700': currentTheme === Theme.light,
'text-gray-300': currentTheme === Theme.dark,
}),
themeToggle: cn('flex h-10 w-10 items-center justify-center rounded-full shadow-md backdrop-blur-sm transition-all duration-300', {
themeToggle: cn('flex items-center justify-center w-10 h-10 rounded-full transition-all duration-300 shadow-md backdrop-blur-sm', {
'bg-white/80 hover:bg-white hover:shadow-lg text-gray-700 border border-gray-200': currentTheme === Theme.light,
'bg-slate-800/80 hover:bg-slate-700 hover:shadow-lg text-yellow-300 border border-slate-600': currentTheme === Theme.dark,
}),
@ -501,7 +512,7 @@ const Flowchart = React.forwardRef((props: {
// Style classes for look options
const getLookButtonClass = (lookType: 'classic' | 'handDrawn') => {
return cn(
'system-sm-medium mb-4 flex h-8 w-[calc((100%-8px)/2)] cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary',
'flex items-center justify-center mb-4 w-[calc((100%-8px)/2)] h-8 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg cursor-pointer system-sm-medium text-text-secondary',
look === lookType && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary',
currentTheme === Theme.dark && 'border-slate-600 bg-slate-800 text-slate-300',
look === lookType && currentTheme === Theme.dark && 'border-blue-500 bg-slate-700 text-white',
@ -512,7 +523,7 @@ const Flowchart = React.forwardRef((props: {
<div ref={ref as React.RefObject<HTMLDivElement>} className={themeClasses.container}>
<div className={themeClasses.segmented}>
<div className="msh-segmented-group">
<label className="msh-segmented-item m-2 flex w-[200px] items-center space-x-1">
<label className="msh-segmented-item flex items-center space-x-1 m-2 w-[200px]">
<div
key='classic'
className={getLookButtonClass('classic')}
@ -534,7 +545,7 @@ const Flowchart = React.forwardRef((props: {
<div ref={containerRef} style={{ position: 'absolute', visibility: 'hidden', height: 0, overflow: 'hidden' }} />
{isLoading && !svgCode && (
<div className='px-[26px] py-4'>
<div className='py-4 px-[26px]'>
<LoadingAnim type='text'/>
{!isCodeComplete && (
<div className="mt-2 text-sm text-gray-500">
@ -546,7 +557,7 @@ const Flowchart = React.forwardRef((props: {
{svgCode && (
<div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={() => setImagePreviewUrl(svgCode)}>
<div className="absolute bottom-2 left-2 z-[100]">
<div className="absolute left-2 bottom-2 z-[100]">
<button
onClick={(e) => {
e.stopPropagation()

View File

@ -22,6 +22,10 @@ export function preprocessMermaidCode(code: string): string {
.replace(/section\s+([^:]+):/g, (match, sectionName) => `section ${sectionName}`)
// Fix common syntax issues
.replace(/fifopacket/g, 'rect')
// Ensure graph has direction
.replace(/^graph\s+((?:TB|BT|RL|LR)*)/, (match, direction) => {
return direction ? match : 'graph TD'
})
// Clean up empty lines and extra spaces
.trim()
}
@ -32,9 +36,9 @@ export function preprocessMermaidCode(code: string): string {
export function prepareMermaidCode(code: string, style: 'classic' | 'handDrawn'): string {
let finalCode = preprocessMermaidCode(code)
// Special handling for gantt charts
if (finalCode.trim().startsWith('gantt')) {
// For gantt charts, preserve the structure exactly as is
// Special handling for gantt charts and mindmaps
if (finalCode.trim().startsWith('gantt') || finalCode.trim().startsWith('mindmap')) {
// For gantt charts and mindmaps, preserve the structure exactly as is
return finalCode
}
@ -173,8 +177,15 @@ export function isMermaidCodeComplete(code: string): boolean {
return lines.length >= 3
}
// Special handling for mindmaps
if (trimmedCode.startsWith('mindmap')) {
// For mindmaps, check if it has at least a root node
const lines = trimmedCode.split('\n').filter(line => line.trim().length > 0)
return lines.length >= 2
}
// Check for basic syntax structure
const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram)/.test(trimmedCode)
const hasValidStart = /^(graph|flowchart|sequenceDiagram|classDiagram|classDef|class|stateDiagram|gantt|pie|er|journey|requirementDiagram|mindmap)/.test(trimmedCode)
// Check for balanced brackets and parentheses
const isBalanced = (() => {

View File

@ -37,14 +37,15 @@ export default function Radio({
const isChecked = groupContext ? groupContext.value === value : checked
const divClassName = `
flex items-center py-1 relative
px-7 cursor-pointer hover:bg-gray-200 rounded
px-7 cursor-pointer text-text-secondary rounded
bg-components-option-card-option-bg hover:bg-components-option-card-option-bg-hover hover:shadow-xs
`
return (
<div className={cn(
s.label,
disabled ? s.disabled : '',
isChecked ? 'bg-white shadow' : '',
isChecked ? 'bg-components-option-card-option-bg-hover shadow-xs' : '',
divClassName,
className)}
onClick={() => handleChange(value)}

View File

@ -40,7 +40,7 @@ const TabSlider: FC<TabSliderProps> = ({
const newIndex = options.findIndex(option => option.value === value)
setActiveIndex(newIndex)
updateSliderStyle(newIndex)
}, [value, options, pluginList])
}, [value, options, pluginList?.total])
return (
<div className={cn(className, 'relative inline-flex items-center justify-center rounded-[10px] bg-components-segmented-control-bg-normal p-0.5')}>
@ -69,13 +69,13 @@ const TabSlider: FC<TabSliderProps> = ({
{option.text}
{/* if no plugin installed, the badge won't show */}
{option.value === 'plugins'
&& (pluginList?.plugins.length ?? 0) > 0
&& (pluginList?.total ?? 0) > 0
&& <Badge
size='s'
uppercase={true}
state={BadgeState.Default}
>
{pluginList?.plugins.length}
{pluginList?.total}
</Badge>
}
</div>

View File

@ -24,6 +24,7 @@ import {
} from '@/service/common'
import { useAppContext } from '@/context/app-context'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ALLOW_FILE_EXTENSIONS = ['svg', 'png']
@ -39,6 +40,7 @@ const CustomWebAppBrand = () => {
const [fileId, setFileId] = useState('')
const [imgKey, setImgKey] = useState(Date.now())
const [uploadProgress, setUploadProgress] = useState(0)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const isSandbox = enableBilling && plan.type === Plan.sandbox
const uploading = uploadProgress > 0 && uploadProgress < 100
const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || ''
@ -128,7 +130,7 @@ const CustomWebAppBrand = () => {
<div className='system-xs-regular text-text-tertiary'>{t('custom.webapp.changeLogoTip')}</div>
</div>
<div className='flex items-center'>
{(uploadDisabled || (!webappLogo && !webappBrandRemoved)) && (
{(!uploadDisabled && webappLogo && !webappBrandRemoved) && (
<>
<Button
variant='ghost'
@ -244,9 +246,12 @@ const CustomWebAppBrand = () => {
{!webappBrandRemoved && (
<>
<div className='system-2xs-medium-uppercase text-text-tertiary'>POWERED BY</div>
{webappLogo
? <img src={`${webappLogo}?hash=${imgKey}`} alt='logo' className='block h-5 w-auto' />
: <DifyLogo size='small' />
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt='logo' className='block h-5 w-auto' />
: webappLogo
? <img src={`${webappLogo}?hash=${imgKey}`} alt='logo' className='block h-5 w-auto' />
: <DifyLogo size='small' />
}
</>
)}
@ -303,9 +308,12 @@ const CustomWebAppBrand = () => {
{!webappBrandRemoved && (
<>
<div className='system-2xs-medium-uppercase text-text-tertiary'>POWERED BY</div>
{webappLogo
? <img src={`${webappLogo}?hash=${imgKey}`} alt='logo' className='block h-5 w-auto' />
: <DifyLogo size='small' />
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt='logo' className='block h-5 w-auto' />
: webappLogo
? <img src={`${webappLogo}?hash=${imgKey}`} alt='logo' className='block h-5 w-auto' />
: <DifyLogo size='small' />
}
</>
)}

View File

@ -865,10 +865,10 @@ const StepTwo = ({
<>
<CustomDialog show={isQAConfirmDialogOpen} onClose={() => setIsQAConfirmDialogOpen(false)} className='w-[432px]'>
<header className='mb-4 pt-6'>
<h2 className='text-lg font-semibold'>
<h2 className='text-lg font-semibold text-text-primary'>
{t('datasetCreation.stepTwo.qaSwitchHighQualityTipTitle')}
</h2>
<p className='mt-2 text-sm font-normal'>
<p className='mt-2 text-sm font-normal text-text-secondary'>
{t('datasetCreation.stepTwo.qaSwitchHighQualityTipContent')}
</p>
</header>
@ -928,7 +928,7 @@ const StepTwo = ({
</div>
)}
{hasSetIndexType && indexType === IndexingType.ECONOMICAL && (
<div className='system-xs-medium mt-2'>
<div className='system-xs-medium mt-2 text-text-tertiary'>
{t('datasetCreation.stepTwo.indexSettingTip')}
<Link className='text-text-accent' href={`/datasets/${datasetId}/settings`}>{t('datasetCreation.stepTwo.datasetSettingLink')}</Link>
</div>

View File

@ -50,20 +50,20 @@ const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
return (
<div className='mt-6'>
<div className='text-sm font-medium text-gray-900'>{t('share.generation.csvStructureTitle')}</div>
<div className='text-sm font-medium text-text-primary'>{t('share.generation.csvStructureTitle')}</div>
<div className='mt-2 max-h-[500px] overflow-auto'>
{docForm === ChunkingMode.qa && (
<table className='w-full table-fixed border-separate border-spacing-0 rounded-lg border border-gray-200 text-xs'>
<thead className='text-gray-500'>
<table className='w-full table-fixed border-separate border-spacing-0 rounded-lg border border-divider-subtle text-xs'>
<thead className='text-text-secondary'>
<tr>
<td className='h-9 border-b border-gray-200 pl-3 pr-2'>{t('datasetDocuments.list.batchModal.question')}</td>
<td className='h-9 border-b border-gray-200 pl-3 pr-2'>{t('datasetDocuments.list.batchModal.answer')}</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2'>{t('datasetDocuments.list.batchModal.question')}</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2'>{t('datasetDocuments.list.batchModal.answer')}</td>
</tr>
</thead>
<tbody className='text-gray-700'>
<tbody className='text-text-tertiary'>
<tr>
<td className='h-9 border-b border-gray-100 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.question')} 1</td>
<td className='h-9 border-b border-gray-100 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.answer')} 1</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.question')} 1</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.answer')} 1</td>
</tr>
<tr>
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.question')} 2</td>
@ -73,15 +73,15 @@ const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
</table>
)}
{docForm === ChunkingMode.text && (
<table className='w-full table-fixed border-separate border-spacing-0 rounded-lg border border-gray-200 text-xs'>
<thead className='text-gray-500'>
<table className='w-full table-fixed border-separate border-spacing-0 rounded-lg border border-divider-subtle text-xs'>
<thead className='text-text-secondary'>
<tr>
<td className='h-9 border-b border-gray-200 pl-3 pr-2'>{t('datasetDocuments.list.batchModal.contentTitle')}</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2'>{t('datasetDocuments.list.batchModal.contentTitle')}</td>
</tr>
</thead>
<tbody className='text-gray-700'>
<tbody className='text-text-tertiary'>
<tr>
<td className='h-9 border-b border-gray-100 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.content')} 1</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.content')} 1</td>
</tr>
<tr>
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.content')} 2</td>

View File

@ -93,29 +93,29 @@ const CSVUploader: FC<Props> = ({
/>
<div ref={dropRef}>
{!file && (
<div className={cn('flex h-20 items-center rounded-xl border border-dashed border-gray-200 bg-gray-50 text-sm font-normal', dragging && 'border border-[#B2CCFF] bg-[#F5F8FF]')}>
<div className={cn('flex h-20 items-center rounded-xl border border-dashed border-components-panel-border bg-components-panel-bg-blur text-sm font-normal', dragging && 'border border-divider-subtle bg-components-panel-on-panel-item-bg-hover')}>
<div className='flex w-full items-center justify-center space-x-2'>
<CSVIcon className="shrink-0" />
<div className='text-gray-500'>
<div className='text-text-secondary'>
{t('datasetDocuments.list.batchModal.csvUploadTitle')}
<span className='cursor-pointer text-primary-400' onClick={selectHandle}>{t('datasetDocuments.list.batchModal.browse')}</span>
<span className='cursor-pointer text-text-accent' onClick={selectHandle}>{t('datasetDocuments.list.batchModal.browse')}</span>
</div>
</div>
{dragging && <div ref={dragRef} className='absolute left-0 top-0 h-full w-full' />}
</div>
)}
{file && (
<div className={cn('group flex h-20 items-center rounded-xl border border-gray-200 bg-gray-50 px-6 text-sm font-normal', 'hover:border-[#B2CCFF] hover:bg-[#F5F8FF]')}>
<div className={cn('group flex h-20 items-center rounded-xl border border-components-panel-border bg-components-panel-bg-blur px-6 text-sm font-normal', 'hover:border-divider-subtle hover:bg-components-panel-on-panel-item-bg-hover')}>
<CSVIcon className="shrink-0" />
<div className='ml-2 flex w-0 grow'>
<span className='max-w-[calc(100%_-_30px)] overflow-hidden text-ellipsis whitespace-nowrap text-gray-800'>{file.name.replace(/.csv$/, '')}</span>
<span className='shrink-0 text-gray-500'>.csv</span>
<span className='max-w-[calc(100%_-_30px)] overflow-hidden text-ellipsis whitespace-nowrap text-text-primary'>{file.name.replace(/.csv$/, '')}</span>
<span className='shrink-0 text-text-secondary'>.csv</span>
</div>
<div className='hidden items-center group-hover:flex'>
<Button onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
<div className='mx-2 h-4 w-px bg-gray-200' />
<div className='mx-2 h-4 w-px bg-text-secondary' />
<div className='cursor-pointer p-2' onClick={removeFile}>
<RiDeleteBinLine className='h-4 w-4 text-gray-500' />
<RiDeleteBinLine className='h-4 w-4 text-text-secondary' />
</div>
</div>
</div>

View File

@ -41,9 +41,9 @@ const BatchModal: FC<IBatchModalProps> = ({
return (
<Modal isShow={isShow} onClose={noop} className='!max-w-[520px] !rounded-xl px-8 py-6'>
<div className='relative pb-1 text-xl font-medium leading-[30px] text-gray-900'>{t('datasetDocuments.list.batchModal.title')}</div>
<div className='relative pb-1 text-xl font-medium leading-[30px] text-text-primary'>{t('datasetDocuments.list.batchModal.title')}</div>
<div className='absolute right-4 top-4 cursor-pointer p-2' onClick={onCancel}>
<RiCloseLine className='h-4 w-4 text-gray-500' />
<RiCloseLine className='h-4 w-4 text-text-secondary' />
</div>
<CSVUploader
file={currentCSV}

View File

@ -91,6 +91,7 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
customComponent: isFullDocMode && CustomButton,
})
handleCancel('add')
setContent('')
if (isFullDocMode) {
refreshTimer.current = setTimeout(() => {
onSave()

View File

@ -118,6 +118,9 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
customComponent: CustomButton,
})
handleCancel('add')
setQuestion('')
setAnswer('')
setKeywords([])
refreshTimer.current = setTimeout(() => {
onSave()
}, 3000)

View File

@ -232,7 +232,7 @@ const Form = () => {
<div className='p-1'>
<AlertTriangle className='size-4 text-text-warning-secondary' />
</div>
<span className='system-xs-medium'>{t('datasetSettings.form.upgradeHighQualityTip')}</span>
<span className='system-xs-medium text-text-warning-secondary'>{t('datasetSettings.form.upgradeHighQualityTip')}</span>
</div>}
</div>
</div>

View File

@ -9,6 +9,7 @@ import type { LangGeniusVersionResponse } from '@/models/common'
import { IS_CE_EDITION } from '@/config'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import { noop } from 'lodash-es'
import { useGlobalPublicStore } from '@/context/global-public-context'
type IAccountSettingProps = {
langeniusVersionInfo: LangGeniusVersionResponse
@ -21,6 +22,7 @@ export default function AccountAbout({
}: IAccountSettingProps) {
const { t } = useTranslation()
const isLatest = langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
return (
<Modal
@ -33,7 +35,14 @@ export default function AccountAbout({
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
</div>
<div className='flex flex-col items-center gap-4 py-8'>
<DifyLogo size='large' className='mx-auto' />
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img
src={systemFeatures.branding.workspace_logo}
className='block h-7 w-auto object-contain'
alt='logo'
/>
: <DifyLogo size='large' className='mx-auto' />}
<div className='text-center text-xs font-normal text-text-tertiary'>Version {langeniusVersionInfo?.current_version}</div>
<div className='flex flex-col items-center gap-2 text-center text-xs font-normal text-text-secondary'>
<div>© {dayjs().year()} LangGenius, Inc., Contributors.</div>

View File

@ -5,7 +5,7 @@ import type {
} from '../declarations'
import { useLanguage } from '../hooks'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
import { OpenaiBlue, OpenaiTeal, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
import cn from '@/utils/classnames'
import { renderI18nObject } from '@/i18n'
@ -22,6 +22,10 @@ const ModelIcon: FC<ModelIconProps> = ({
isDeprecated = false,
}) => {
const language = useLanguage()
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o'))
return <div className='flex items-center justify-center'><OpenaiYellow className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4.1'))
return <div className='flex items-center justify-center'><OpenaiTeal className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4o'))
return <div className='flex items-center justify-center'><OpenaiBlue className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('gpt-4'))

View File

@ -1,6 +1,5 @@
import type { FC } from 'react'
import type { ModelProvider } from '../declarations'
import { basePath } from '@/utils/var'
import { useLanguage } from '../hooks'
import { Openai } from '@/app/components/base/icons/src/vender/other'
import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm'
@ -41,7 +40,7 @@ const ProviderIcon: FC<ProviderIconProps> = ({
<div className={cn('inline-flex items-center gap-2', className)}>
<img
alt='provider-icon'
src={basePath + renderI18nObject(provider.icon_small, language)}
src={renderI18nObject(provider.icon_small, language)}
className='h-6 w-6'
/>
<div className='system-md-semibold text-text-primary'>

View File

@ -21,6 +21,7 @@ import { useModalContext } from '@/context/modal-context'
import PlanBadge from './plan-badge'
import LicenseNav from './license-env'
import { Plan } from '../billing/type'
import { useGlobalPublicStore } from '@/context/global-public-context'
const navClassName = `
flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@ -36,6 +37,7 @@ const Header = () => {
const [isShowNavMenu, { toggle, setFalse: hideNavMenu }] = useBoolean(false)
const { enableBilling, plan } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const isFreePlan = plan.type === Plan.sandbox
const handlePlanClick = useCallback(() => {
if (isFreePlan)
@ -61,7 +63,13 @@ const Header = () => {
!isMobile
&& <div className='flex shrink-0 items-center gap-1.5 self-stretch pl-3'>
<Link href="/apps" className='flex h-8 shrink-0 items-center justify-center gap-2 px-0.5'>
<DifyLogo />
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img
src={systemFeatures.branding.workspace_logo}
className='block h-[22px] w-auto object-contain'
alt='logo'
/>
: <DifyLogo />}
</Link>
<div className='font-light text-divider-deep'>/</div>
<div className='flex items-center gap-0.5'>
@ -76,7 +84,13 @@ const Header = () => {
{isMobile && (
<div className='flex'>
<Link href="/apps" className='mr-4 flex items-center'>
<DifyLogo />
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img
src={systemFeatures.branding.workspace_logo}
className='block h-[22px] w-auto object-contain'
alt='logo'
/>
: <DifyLogo />}
</Link>
<div className='font-light text-divider-deep'>/</div>
{enableBilling ? <PlanBadge allowHover sandboxAsUpgrade plan={plan.type} onClick={handlePlanClick} /> : <LicenseNav />}

View File

@ -83,7 +83,12 @@ const InstallByDSLList: FC<Props> = ({
useEffect(() => {
if (!isFetchingMarketplaceDataById && infoGetById?.data.plugins) {
const payloads = infoGetById?.data.plugins
const sortedList = allPlugins.filter(d => d.type === 'marketplace').map((d) => {
const p = d as GitHubItemAndMarketPlaceDependency
const id = p.value.marketplace_plugin_unique_identifier?.split(':')[0]
return infoGetById.data.plugins.find(item => item.plugin_id === id)!
})
const payloads = sortedList
const failedIndex: number[] = []
const nextPlugins = produce(pluginsRef.current, (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => {

View File

@ -9,6 +9,7 @@ import InstallMulti from './install-multi'
import { useInstallOrUpdate } from '@/service/use-plugins'
import useRefreshPluginList from '../../hooks/use-refresh-plugin-list'
import { useCanInstallPluginFromMarketplace } from '@/app/components/plugins/plugin-page/use-permission'
import { useMittContextSelector } from '@/context/mitt-context'
const i18nPrefix = 'plugin.installModal'
type Props = {
@ -29,6 +30,7 @@ const Install: FC<Props> = ({
isHideButton,
}) => {
const { t } = useTranslation()
const emit = useMittContextSelector(s => s.emit)
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
const selectedPluginsNum = selectedPlugins.length
@ -63,8 +65,12 @@ const Install: FC<Props> = ({
})
}))
const hasInstallSuccess = res.some(r => r.success)
if (hasInstallSuccess)
if (hasInstallSuccess) {
refreshPluginList(undefined, true)
emit('plugin:install:success', selectedPlugins.map((p) => {
return `${p.plugin_id}/${p.name}`
}))
}
},
})
const handleInstall = () => {

View File

@ -78,7 +78,7 @@ const ActionList = ({
className='w-full'
onClick={() => setShowSettingAuth(true)}
disabled={!isCurrentWorkspaceManager}
>{t('tools.auth.unauthorized')}</Button>
>{t('workflow.nodes.tool.authorize')}</Button>
)}
</div>
<div className='flex flex-col gap-2'>

View File

@ -141,7 +141,7 @@ const MultipleToolSelector = ({
}
panelShowState={panelShowState}
onPanelShowStateChange={setPanelShowState}
isEdit={false}
/>
{value.length === 0 && (
<div className='system-xs-regular flex justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary'>{t('plugin.detailPanel.toolSelector.empty')}</div>
@ -158,6 +158,7 @@ const MultipleToolSelector = ({
onSelect={item => handleConfigure(item, index)}
onDelete={() => handleDelete(index)}
supportEnableSwitch
isEdit
/>
</div>
))}

View File

@ -54,6 +54,7 @@ type Props = {
scope?: string
value?: ToolValue
selectedTools?: ToolValue[]
isEdit?: boolean
onSelect: (tool: {
provider_name: string
tool_name: string
@ -77,6 +78,7 @@ type Props = {
const ToolSelector: FC<Props> = ({
value,
selectedTools,
isEdit,
disabled,
placement = 'left',
offset = 4,
@ -277,7 +279,7 @@ const ToolSelector: FC<Props> = ({
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', !isShowSettingAuth && 'overflow-y-auto pb-2')}>
{!isShowSettingAuth && (
<>
<div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t('plugin.detailPanel.toolSelector.title')}</div>
<div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}</div>
{/* base form */}
<div className='flex flex-col gap-3 px-4 py-2'>
<div className='flex flex-col gap-1'>

View File

@ -1,5 +1,6 @@
'use client'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import type { FilterState } from './filter-management'
import FilterManagement from './filter-management'
import List from './list'
@ -7,14 +8,16 @@ import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstall
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { usePluginPageContext } from './context'
import { useDebounceFn } from 'ahooks'
import Button from '@/app/components/base/button'
import Empty from './empty'
import Loading from '../../base/loading'
import { PluginSource } from '../types'
const PluginsPanel = () => {
const { t } = useTranslation()
const filters = usePluginPageContext(v => v.filters) as FilterState
const setFilters = usePluginPageContext(v => v.setFilters)
const { data: pluginList, isLoading: isPluginListLoading } = useInstalledPluginList()
const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginList()
const { data: installedLatestVersion } = useInstalledLatestVersion(
pluginList?.plugins
.filter(plugin => plugin.source === PluginSource.marketplace)
@ -64,10 +67,16 @@ const PluginsPanel = () => {
/>
</div>
{isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? (
<div className='flex grow flex-wrap content-start items-start gap-2 self-stretch px-12'>
<div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch px-12'>
<div className='w-full'>
<List pluginList={filteredList || []} />
</div>
{!isLastPage && !isFetching && (
<Button onClick={loadNextPage}>
{t('workflow.common.loadMore')}
</Button>
)}
{isFetching && <div className='system-md-semibold text-text-secondary'>{t('appLog.detail.loading')}</div>}
</div>
) : (
<Empty />

View File

@ -325,6 +325,11 @@ export type InstalledPluginListResponse = {
plugins: PluginDetail[]
}
export type InstalledPluginListWithTotalResponse = {
plugins: PluginDetail[]
total: number
}
export type InstalledLatestVersionResponse = {
versions: {
[plugin_id: string]: {

View File

@ -641,11 +641,13 @@ const TextGeneration: FC<IMainProps> = ({
!isPC && resultExisted && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
{systemFeatures.branding.enabled ? (
<img src={systemFeatures.branding.login_page_logo} alt='logo' className='block h-5 w-auto' />
) : (
<DifyLogo size='small' />
)}
{
systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? <img src={systemFeatures.branding.workspace_logo} alt='logo' className='block h-5 w-auto' />
: customConfig?.replace_webapp_logo
? <img src={`${customConfig?.replace_webapp_logo}`} alt='logo' className='block h-5 w-auto' />
: <DifyLogo size='small' />
}
</div>
)}
</div>

View File

@ -148,7 +148,7 @@ const CodeEditor: FC<Props> = ({
{isShowVarPicker && (
<div
ref={popupRef}
className='w-[228px] space-y-1 rounded-lg border border-gray-200 bg-white p-1 shadow-lg'
className='w-[228px] space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg'
style={{
position: 'fixed',
top: popupPosition.y,

View File

@ -43,7 +43,7 @@ const VarReferencePicker: FC<Props> = ({
offset={4}
>
<PortalToFollowElemTrigger onClick={() => setOpen(!open)} className='w-[120px] cursor-pointer'>
<div className='flex h-8 items-center justify-between rounded-lg border-0 bg-components-button-secondary-bg px-2.5 text-[13px] text-text-primary'>
<div className='flex h-8 items-center justify-between rounded-lg border-0 bg-components-input-bg-normal px-2.5 text-[13px] text-text-primary'>
<div className='w-0 grow truncate capitalize' title={value}>{value}</div>
<RiArrowDownSLine className='h-3.5 w-3.5 shrink-0 text-text-secondary' />
</div>

View File

@ -126,7 +126,7 @@ const nodeDefault: NodeDefault<AgentNodeType> = {
}
}
// common params
if (param.required && !payload.agent_parameters?.[param.name]?.value) {
if (param.required && !(payload.agent_parameters?.[param.name]?.value || param.default)) {
return {
isValid: false,
errorMessage: t('workflow.errorMsg.fieldRequired', { field: renderI18nObject(param.label, language) }),

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BodyType, type HttpNodeType, Method } from '../types'
import { BodyPayloadValueType, BodyType, type HttpNodeType, Method } from '../types'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
@ -51,11 +51,16 @@ const parseCurl = (curlCommand: string): { node: HttpNodeType | null; error: str
case '-d':
case '--data':
case '--data-raw':
case '--data-binary':
case '--data-binary': {
if (i + 1 >= args.length)
return { node: null, error: 'Missing data value after -d, --data, --data-raw, or --data-binary.' }
node.body = { type: BodyType.rawText, data: args[++i].replace(/^['"]|['"]$/g, '') }
const bodyPayload = [{
type: BodyPayloadValueType.text,
value: args[++i].replace(/^['"]|['"]$/g, ''),
}]
node.body = { type: BodyType.rawText, data: bodyPayload }
break
}
case '-F':
case '--form': {
if (i + 1 >= args.length)

View File

@ -42,6 +42,12 @@ const useConfig = (id: string, payload: HttpNodeType) => {
data: transformToBodyPayload(bodyData, [BodyType.formData, BodyType.xWwwFormUrlencoded].includes(newInputs.body.type)),
}
}
else if (!bodyData) {
newInputs.body = {
...newInputs.body,
data: [],
}
}
setInputs(newInputs)
setIsDataReady(true)
@ -151,7 +157,7 @@ const useConfig = (id: string, payload: HttpNodeType) => {
inputs.url,
inputs.headers,
inputs.params,
typeof inputs.body.data === 'string' ? inputs.body.data : inputs.body.data.map(item => item.value).join(''),
typeof inputs.body.data === 'string' ? inputs.body.data : inputs.body.data?.map(item => item.value).join(''),
fileVarInputs,
])

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import ConditionValueMethod from './condition-value-method'
import type { ConditionValueMethodProps } from './condition-value-method'
import ConditionVariableSelector from './condition-variable-selector'
import ConditionCommonVariableSelector from './condition-common-variable-selector.tsx'
import ConditionCommonVariableSelector from './condition-common-variable-selector'
import type {
Node,
NodeOutPutVar,

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import ConditionValueMethod from './condition-value-method'
import type { ConditionValueMethodProps } from './condition-value-method'
import ConditionVariableSelector from './condition-variable-selector'
import ConditionCommonVariableSelector from './condition-common-variable-selector.tsx'
import ConditionCommonVariableSelector from './condition-common-variable-selector'
import type {
Node,
NodeOutPutVar,

View File

@ -28,6 +28,7 @@ const CodeEditor: FC<CodeEditorProps> = ({
const { theme } = useTheme()
const monacoRef = useRef<any>(null)
const editorRef = useRef<any>(null)
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (monacoRef.current) {
@ -74,6 +75,19 @@ const CodeEditor: FC<CodeEditorProps> = ({
onUpdate?.(value)
}, [onUpdate])
useEffect(() => {
const resizeObserver = new ResizeObserver(() => {
editorRef.current?.layout()
})
if (containerRef.current)
resizeObserver.observe(containerRef.current)
return () => {
resizeObserver.disconnect()
}
}, [])
return (
<div className={classNames('flex flex-col h-full bg-components-input-bg-normal overflow-hidden', className)}>
<div className='flex items-center justify-between pl-2 pr-1 pt-1'>
@ -102,9 +116,11 @@ const CodeEditor: FC<CodeEditorProps> = ({
</Tooltip>
</div>
</div>
<div className={classNames('relative', editorWrapperClassName)}>
<div
ref={containerRef}
className={classNames('relative overflow-hidden', editorWrapperClassName)}
>
<Editor
height='100%'
defaultLanguage='json'
value={value}
onChange={handleEditorChange}
@ -117,7 +133,6 @@ const CodeEditor: FC<CodeEditorProps> = ({
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'same',
// Add these options
overviewRulerBorder: false,
hideCursorInOverviewRuler: true,
renderLineHighlightOnlyWhenFocus: false,

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