mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 12:37:20 +08:00
Merge remote-tracking branch 'origin/feat/rag-2' into feat/rag-2
This commit is contained in:
commit
a8fbf123e4
@ -1,5 +1,7 @@
|
|||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
|
from libs.exception import BaseHTTPException
|
||||||
|
|
||||||
|
|
||||||
class FilenameNotExistsError(HTTPException):
|
class FilenameNotExistsError(HTTPException):
|
||||||
code = 400
|
code = 400
|
||||||
@ -9,3 +11,27 @@ class FilenameNotExistsError(HTTPException):
|
|||||||
class RemoteFileUploadError(HTTPException):
|
class RemoteFileUploadError(HTTPException):
|
||||||
code = 400
|
code = 400
|
||||||
description = "Error uploading remote file."
|
description = "Error uploading remote file."
|
||||||
|
|
||||||
|
|
||||||
|
class FileTooLargeError(BaseHTTPException):
|
||||||
|
error_code = "file_too_large"
|
||||||
|
description = "File size exceeded. {message}"
|
||||||
|
code = 413
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedFileTypeError(BaseHTTPException):
|
||||||
|
error_code = "unsupported_file_type"
|
||||||
|
description = "File type not allowed."
|
||||||
|
code = 415
|
||||||
|
|
||||||
|
|
||||||
|
class TooManyFilesError(BaseHTTPException):
|
||||||
|
error_code = "too_many_files"
|
||||||
|
description = "Only one file is allowed."
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class NoFileUploadedError(BaseHTTPException):
|
||||||
|
error_code = "no_file_uploaded"
|
||||||
|
description = "Please upload your file."
|
||||||
|
code = 400
|
||||||
|
|||||||
@ -3,9 +3,8 @@ from flask_login import current_user
|
|||||||
from flask_restful import Resource, marshal, marshal_with, reqparse
|
from flask_restful import Resource, marshal, marshal_with, reqparse
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
|
from controllers.common.errors import NoFileUploadedError, TooManyFilesError
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.error import NoFileUploadedError
|
|
||||||
from controllers.console.datasets.error import TooManyFilesError
|
|
||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
cloud_edition_billing_resource_check,
|
cloud_edition_billing_resource_check,
|
||||||
|
|||||||
@ -79,18 +79,6 @@ class ProviderNotSupportSpeechToTextError(BaseHTTPException):
|
|||||||
code = 400
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
class NoFileUploadedError(BaseHTTPException):
|
|
||||||
error_code = "no_file_uploaded"
|
|
||||||
description = "Please upload your file."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class TooManyFilesError(BaseHTTPException):
|
|
||||||
error_code = "too_many_files"
|
|
||||||
description = "Only one file is allowed."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class DraftWorkflowNotExist(BaseHTTPException):
|
class DraftWorkflowNotExist(BaseHTTPException):
|
||||||
error_code = "draft_workflow_not_exist"
|
error_code = "draft_workflow_not_exist"
|
||||||
description = "Draft workflow need to be initialized."
|
description = "Draft workflow need to be initialized."
|
||||||
|
|||||||
@ -27,7 +27,7 @@ from fields.conversation_fields import annotation_fields, message_detail_fields
|
|||||||
from libs.helper import uuid_value
|
from libs.helper import uuid_value
|
||||||
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models.model import AppMode, Conversation, Message, MessageAnnotation
|
from models.model import AppMode, Conversation, Message, MessageAnnotation, MessageFeedback
|
||||||
from services.annotation_service import AppAnnotationService
|
from services.annotation_service import AppAnnotationService
|
||||||
from services.errors.conversation import ConversationNotExistsError
|
from services.errors.conversation import ConversationNotExistsError
|
||||||
from services.errors.message import MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError
|
from services.errors.message import MessageNotExistsError, SuggestedQuestionsAfterAnswerDisabledError
|
||||||
@ -124,17 +124,34 @@ class MessageFeedbackApi(Resource):
|
|||||||
parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json")
|
parser.add_argument("rating", type=str, choices=["like", "dislike", None], location="json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
message_id = str(args["message_id"])
|
||||||
MessageService.create_feedback(
|
|
||||||
app_model=app_model,
|
message = db.session.query(Message).filter(Message.id == message_id, Message.app_id == app_model.id).first()
|
||||||
message_id=str(args["message_id"]),
|
|
||||||
user=current_user,
|
if not message:
|
||||||
rating=args.get("rating"),
|
|
||||||
content=None,
|
|
||||||
)
|
|
||||||
except MessageNotExistsError:
|
|
||||||
raise NotFound("Message Not Exists.")
|
raise NotFound("Message Not Exists.")
|
||||||
|
|
||||||
|
feedback = message.admin_feedback
|
||||||
|
|
||||||
|
if not args["rating"] and feedback:
|
||||||
|
db.session.delete(feedback)
|
||||||
|
elif args["rating"] and feedback:
|
||||||
|
feedback.rating = args["rating"]
|
||||||
|
elif not args["rating"] and not feedback:
|
||||||
|
raise ValueError("rating cannot be None when feedback not exists")
|
||||||
|
else:
|
||||||
|
feedback = MessageFeedback(
|
||||||
|
app_id=app_model.id,
|
||||||
|
conversation_id=message.conversation_id,
|
||||||
|
message_id=message.id,
|
||||||
|
rating=args["rating"],
|
||||||
|
from_source="admin",
|
||||||
|
from_account_id=current_user.id,
|
||||||
|
)
|
||||||
|
db.session.add(feedback)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
return {"result": "success"}
|
return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,30 +1,6 @@
|
|||||||
from libs.exception import BaseHTTPException
|
from libs.exception import BaseHTTPException
|
||||||
|
|
||||||
|
|
||||||
class NoFileUploadedError(BaseHTTPException):
|
|
||||||
error_code = "no_file_uploaded"
|
|
||||||
description = "Please upload your file."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class TooManyFilesError(BaseHTTPException):
|
|
||||||
error_code = "too_many_files"
|
|
||||||
description = "Only one file is allowed."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class FileTooLargeError(BaseHTTPException):
|
|
||||||
error_code = "file_too_large"
|
|
||||||
description = "File size exceeded. {message}"
|
|
||||||
code = 413
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedFileTypeError(BaseHTTPException):
|
|
||||||
error_code = "unsupported_file_type"
|
|
||||||
description = "File type not allowed."
|
|
||||||
code = 415
|
|
||||||
|
|
||||||
|
|
||||||
class DatasetNotInitializedError(BaseHTTPException):
|
class DatasetNotInitializedError(BaseHTTPException):
|
||||||
error_code = "dataset_not_initialized"
|
error_code = "dataset_not_initialized"
|
||||||
description = "The dataset is still being initialized or indexing. Please wait a moment."
|
description = "The dataset is still being initialized or indexing. Please wait a moment."
|
||||||
|
|||||||
@ -76,30 +76,6 @@ class EmailSendIpLimitError(BaseHTTPException):
|
|||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
|
||||||
class FileTooLargeError(BaseHTTPException):
|
|
||||||
error_code = "file_too_large"
|
|
||||||
description = "File size exceeded. {message}"
|
|
||||||
code = 413
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedFileTypeError(BaseHTTPException):
|
|
||||||
error_code = "unsupported_file_type"
|
|
||||||
description = "File type not allowed."
|
|
||||||
code = 415
|
|
||||||
|
|
||||||
|
|
||||||
class TooManyFilesError(BaseHTTPException):
|
|
||||||
error_code = "too_many_files"
|
|
||||||
description = "Only one file is allowed."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class NoFileUploadedError(BaseHTTPException):
|
|
||||||
error_code = "no_file_uploaded"
|
|
||||||
description = "Please upload your file."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class UnauthorizedAndForceLogout(BaseHTTPException):
|
class UnauthorizedAndForceLogout(BaseHTTPException):
|
||||||
error_code = "unauthorized_and_force_logout"
|
error_code = "unauthorized_and_force_logout"
|
||||||
description = "Unauthorized and force logout."
|
description = "Unauthorized and force logout."
|
||||||
|
|||||||
@ -8,7 +8,13 @@ from werkzeug.exceptions import Forbidden
|
|||||||
import services
|
import services
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from constants import DOCUMENT_EXTENSIONS
|
from constants import DOCUMENT_EXTENSIONS
|
||||||
from controllers.common.errors import FilenameNotExistsError
|
from controllers.common.errors import (
|
||||||
|
FilenameNotExistsError,
|
||||||
|
FileTooLargeError,
|
||||||
|
NoFileUploadedError,
|
||||||
|
TooManyFilesError,
|
||||||
|
UnsupportedFileTypeError,
|
||||||
|
)
|
||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
cloud_edition_billing_resource_check,
|
cloud_edition_billing_resource_check,
|
||||||
@ -18,13 +24,6 @@ from fields.file_fields import file_fields, upload_config_fields
|
|||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|
||||||
from .error import (
|
|
||||||
FileTooLargeError,
|
|
||||||
NoFileUploadedError,
|
|
||||||
TooManyFilesError,
|
|
||||||
UnsupportedFileTypeError,
|
|
||||||
)
|
|
||||||
|
|
||||||
PREVIEW_WORDS_LIMIT = 3000
|
PREVIEW_WORDS_LIMIT = 3000
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -7,18 +7,17 @@ from flask_restful import Resource, marshal_with, reqparse
|
|||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.common import helpers
|
from controllers.common import helpers
|
||||||
from controllers.common.errors import RemoteFileUploadError
|
from controllers.common.errors import (
|
||||||
|
FileTooLargeError,
|
||||||
|
RemoteFileUploadError,
|
||||||
|
UnsupportedFileTypeError,
|
||||||
|
)
|
||||||
from core.file import helpers as file_helpers
|
from core.file import helpers as file_helpers
|
||||||
from core.helper import ssrf_proxy
|
from core.helper import ssrf_proxy
|
||||||
from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields
|
from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|
||||||
from .error import (
|
|
||||||
FileTooLargeError,
|
|
||||||
UnsupportedFileTypeError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteFileInfoApi(Resource):
|
class RemoteFileInfoApi(Resource):
|
||||||
@marshal_with(remote_file_info_fields)
|
@marshal_with(remote_file_info_fields)
|
||||||
|
|||||||
@ -7,15 +7,15 @@ from sqlalchemy import select
|
|||||||
from werkzeug.exceptions import Unauthorized
|
from werkzeug.exceptions import Unauthorized
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.common.errors import FilenameNotExistsError
|
from controllers.common.errors import (
|
||||||
from controllers.console import api
|
FilenameNotExistsError,
|
||||||
from controllers.console.admin import admin_required
|
|
||||||
from controllers.console.datasets.error import (
|
|
||||||
FileTooLargeError,
|
FileTooLargeError,
|
||||||
NoFileUploadedError,
|
NoFileUploadedError,
|
||||||
TooManyFilesError,
|
TooManyFilesError,
|
||||||
UnsupportedFileTypeError,
|
UnsupportedFileTypeError,
|
||||||
)
|
)
|
||||||
|
from controllers.console import api
|
||||||
|
from controllers.console.admin import admin_required
|
||||||
from controllers.console.error import AccountNotLinkTenantError
|
from controllers.console.error import AccountNotLinkTenantError
|
||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (
|
||||||
account_initialization_required,
|
account_initialization_required,
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
from libs.exception import BaseHTTPException
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedFileTypeError(BaseHTTPException):
|
|
||||||
error_code = "unsupported_file_type"
|
|
||||||
description = "File type not allowed."
|
|
||||||
code = 415
|
|
||||||
@ -5,8 +5,8 @@ from flask_restful import Resource, reqparse
|
|||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
|
from controllers.common.errors import UnsupportedFileTypeError
|
||||||
from controllers.files import api
|
from controllers.files import api
|
||||||
from controllers.files.error import UnsupportedFileTypeError
|
|
||||||
from services.account_service import TenantService
|
from services.account_service import TenantService
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|
||||||
|
|||||||
@ -4,8 +4,8 @@ from flask import Response
|
|||||||
from flask_restful import Resource, reqparse
|
from flask_restful import Resource, reqparse
|
||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
|
from controllers.common.errors import UnsupportedFileTypeError
|
||||||
from controllers.files import api
|
from controllers.files import api
|
||||||
from controllers.files.error import UnsupportedFileTypeError
|
|
||||||
from core.tools.signature import verify_tool_file_signature
|
from core.tools.signature import verify_tool_file_signature
|
||||||
from core.tools.tool_file_manager import ToolFileManager
|
from core.tools.tool_file_manager import ToolFileManager
|
||||||
from models import db as global_db
|
from models import db as global_db
|
||||||
|
|||||||
@ -5,11 +5,13 @@ from flask_restful import Resource, marshal_with
|
|||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
import services
|
import services
|
||||||
|
from controllers.common.errors import (
|
||||||
|
FileTooLargeError,
|
||||||
|
UnsupportedFileTypeError,
|
||||||
|
)
|
||||||
from controllers.console.wraps import setup_required
|
from controllers.console.wraps import setup_required
|
||||||
from controllers.files import api
|
from controllers.files import api
|
||||||
from controllers.files.error import UnsupportedFileTypeError
|
|
||||||
from controllers.inner_api.plugin.wraps import get_user
|
from controllers.inner_api.plugin.wraps import get_user
|
||||||
from controllers.service_api.app.error import FileTooLargeError
|
|
||||||
from core.file.helpers import verify_plugin_file_signature
|
from core.file.helpers import verify_plugin_file_signature
|
||||||
from core.tools.tool_file_manager import ToolFileManager
|
from core.tools.tool_file_manager import ToolFileManager
|
||||||
from fields.file_fields import file_fields
|
from fields.file_fields import file_fields
|
||||||
|
|||||||
@ -85,30 +85,6 @@ class ProviderNotSupportSpeechToTextError(BaseHTTPException):
|
|||||||
code = 400
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
class NoFileUploadedError(BaseHTTPException):
|
|
||||||
error_code = "no_file_uploaded"
|
|
||||||
description = "Please upload your file."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class TooManyFilesError(BaseHTTPException):
|
|
||||||
error_code = "too_many_files"
|
|
||||||
description = "Only one file is allowed."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class FileTooLargeError(BaseHTTPException):
|
|
||||||
error_code = "file_too_large"
|
|
||||||
description = "File size exceeded. {message}"
|
|
||||||
code = 413
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedFileTypeError(BaseHTTPException):
|
|
||||||
error_code = "unsupported_file_type"
|
|
||||||
description = "File type not allowed."
|
|
||||||
code = 415
|
|
||||||
|
|
||||||
|
|
||||||
class FileNotFoundError(BaseHTTPException):
|
class FileNotFoundError(BaseHTTPException):
|
||||||
error_code = "file_not_found"
|
error_code = "file_not_found"
|
||||||
description = "The requested file was not found."
|
description = "The requested file was not found."
|
||||||
|
|||||||
@ -2,14 +2,14 @@ from flask import request
|
|||||||
from flask_restful import Resource, marshal_with
|
from flask_restful import Resource, marshal_with
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.common.errors import FilenameNotExistsError
|
from controllers.common.errors import (
|
||||||
from controllers.service_api import api
|
FilenameNotExistsError,
|
||||||
from controllers.service_api.app.error import (
|
|
||||||
FileTooLargeError,
|
FileTooLargeError,
|
||||||
NoFileUploadedError,
|
NoFileUploadedError,
|
||||||
TooManyFilesError,
|
TooManyFilesError,
|
||||||
UnsupportedFileTypeError,
|
UnsupportedFileTypeError,
|
||||||
)
|
)
|
||||||
|
from controllers.service_api import api
|
||||||
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token
|
||||||
from fields.file_fields import file_fields
|
from fields.file_fields import file_fields
|
||||||
from models.model import App, EndUser
|
from models.model import App, EndUser
|
||||||
|
|||||||
@ -6,15 +6,15 @@ from sqlalchemy import desc, select
|
|||||||
from werkzeug.exceptions import Forbidden, NotFound
|
from werkzeug.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.common.errors import FilenameNotExistsError
|
from controllers.common.errors import (
|
||||||
from controllers.service_api import api
|
FilenameNotExistsError,
|
||||||
from controllers.service_api.app.error import (
|
|
||||||
FileTooLargeError,
|
FileTooLargeError,
|
||||||
NoFileUploadedError,
|
NoFileUploadedError,
|
||||||
ProviderNotInitializeError,
|
|
||||||
TooManyFilesError,
|
TooManyFilesError,
|
||||||
UnsupportedFileTypeError,
|
UnsupportedFileTypeError,
|
||||||
)
|
)
|
||||||
|
from controllers.service_api import api
|
||||||
|
from controllers.service_api.app.error import ProviderNotInitializeError
|
||||||
from controllers.service_api.dataset.error import (
|
from controllers.service_api.dataset.error import (
|
||||||
ArchivedDocumentImmutableError,
|
ArchivedDocumentImmutableError,
|
||||||
DocumentIndexingError,
|
DocumentIndexingError,
|
||||||
|
|||||||
@ -1,30 +1,6 @@
|
|||||||
from libs.exception import BaseHTTPException
|
from libs.exception import BaseHTTPException
|
||||||
|
|
||||||
|
|
||||||
class NoFileUploadedError(BaseHTTPException):
|
|
||||||
error_code = "no_file_uploaded"
|
|
||||||
description = "Please upload your file."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class TooManyFilesError(BaseHTTPException):
|
|
||||||
error_code = "too_many_files"
|
|
||||||
description = "Only one file is allowed."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class FileTooLargeError(BaseHTTPException):
|
|
||||||
error_code = "file_too_large"
|
|
||||||
description = "File size exceeded. {message}"
|
|
||||||
code = 413
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedFileTypeError(BaseHTTPException):
|
|
||||||
error_code = "unsupported_file_type"
|
|
||||||
description = "File type not allowed."
|
|
||||||
code = 415
|
|
||||||
|
|
||||||
|
|
||||||
class DatasetNotInitializedError(BaseHTTPException):
|
class DatasetNotInitializedError(BaseHTTPException):
|
||||||
error_code = "dataset_not_initialized"
|
error_code = "dataset_not_initialized"
|
||||||
description = "The dataset is still being initialized or indexing. Please wait a moment."
|
description = "The dataset is still being initialized or indexing. Please wait a moment."
|
||||||
|
|||||||
@ -97,30 +97,6 @@ class ProviderNotSupportSpeechToTextError(BaseHTTPException):
|
|||||||
code = 400
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
class NoFileUploadedError(BaseHTTPException):
|
|
||||||
error_code = "no_file_uploaded"
|
|
||||||
description = "Please upload your file."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class TooManyFilesError(BaseHTTPException):
|
|
||||||
error_code = "too_many_files"
|
|
||||||
description = "Only one file is allowed."
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class FileTooLargeError(BaseHTTPException):
|
|
||||||
error_code = "file_too_large"
|
|
||||||
description = "File size exceeded. {message}"
|
|
||||||
code = 413
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedFileTypeError(BaseHTTPException):
|
|
||||||
error_code = "unsupported_file_type"
|
|
||||||
description = "File type not allowed."
|
|
||||||
code = 415
|
|
||||||
|
|
||||||
|
|
||||||
class WebAppAuthRequiredError(BaseHTTPException):
|
class WebAppAuthRequiredError(BaseHTTPException):
|
||||||
error_code = "web_sso_auth_required"
|
error_code = "web_sso_auth_required"
|
||||||
description = "Web app authentication required."
|
description = "Web app authentication required."
|
||||||
|
|||||||
@ -2,8 +2,13 @@ from flask import request
|
|||||||
from flask_restful import marshal_with
|
from flask_restful import marshal_with
|
||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.common.errors import FilenameNotExistsError
|
from controllers.common.errors import (
|
||||||
from controllers.web.error import FileTooLargeError, NoFileUploadedError, TooManyFilesError, UnsupportedFileTypeError
|
FilenameNotExistsError,
|
||||||
|
FileTooLargeError,
|
||||||
|
NoFileUploadedError,
|
||||||
|
TooManyFilesError,
|
||||||
|
UnsupportedFileTypeError,
|
||||||
|
)
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
from fields.file_fields import file_fields
|
from fields.file_fields import file_fields
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|||||||
@ -5,15 +5,17 @@ from flask_restful import marshal_with, reqparse
|
|||||||
|
|
||||||
import services
|
import services
|
||||||
from controllers.common import helpers
|
from controllers.common import helpers
|
||||||
from controllers.common.errors import RemoteFileUploadError
|
from controllers.common.errors import (
|
||||||
|
FileTooLargeError,
|
||||||
|
RemoteFileUploadError,
|
||||||
|
UnsupportedFileTypeError,
|
||||||
|
)
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
from core.file import helpers as file_helpers
|
from core.file import helpers as file_helpers
|
||||||
from core.helper import ssrf_proxy
|
from core.helper import ssrf_proxy
|
||||||
from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields
|
from fields.file_fields import file_fields_with_signed_url, remote_file_info_fields
|
||||||
from services.file_service import FileService
|
from services.file_service import FileService
|
||||||
|
|
||||||
from .error import FileTooLargeError, UnsupportedFileTypeError
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteFileInfoApi(WebApiResource):
|
class RemoteFileInfoApi(WebApiResource):
|
||||||
@marshal_with(remote_file_info_fields)
|
@marshal_with(remote_file_info_fields)
|
||||||
|
|||||||
@ -74,6 +74,7 @@ from core.workflow.system_variable import SystemVariable
|
|||||||
from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager
|
from core.workflow.workflow_cycle_manager import CycleManagerWorkflowInfo, WorkflowCycleManager
|
||||||
from events.message_event import message_was_created
|
from events.message_event import message_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
|
from libs.datetime_utils import naive_utc_now
|
||||||
from models import Conversation, EndUser, Message, MessageFile
|
from models import Conversation, EndUser, Message, MessageFile
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from models.enums import CreatorUserRole
|
from models.enums import CreatorUserRole
|
||||||
@ -896,6 +897,7 @@ class AdvancedChatAppGenerateTaskPipeline:
|
|||||||
def _save_message(self, *, session: Session, graph_runtime_state: Optional[GraphRuntimeState] = None) -> None:
|
def _save_message(self, *, session: Session, graph_runtime_state: Optional[GraphRuntimeState] = None) -> None:
|
||||||
message = self._get_message(session=session)
|
message = self._get_message(session=session)
|
||||||
message.answer = self._task_state.answer
|
message.answer = self._task_state.answer
|
||||||
|
message.updated_at = naive_utc_now()
|
||||||
message.provider_response_latency = time.perf_counter() - self._base_task_pipeline._start_at
|
message.provider_response_latency = time.perf_counter() - self._base_task_pipeline._start_at
|
||||||
message.message_metadata = self._task_state.metadata.model_dump_json()
|
message.message_metadata = self._task_state.metadata.model_dump_json()
|
||||||
message_files = [
|
message_files = [
|
||||||
|
|||||||
@ -57,6 +57,7 @@ from core.prompt.utils.prompt_message_util import PromptMessageUtil
|
|||||||
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
|
from core.prompt.utils.prompt_template_parser import PromptTemplateParser
|
||||||
from events.message_event import message_was_created
|
from events.message_event import message_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
|
from libs.datetime_utils import naive_utc_now
|
||||||
from models.model import AppMode, Conversation, Message, MessageAgentThought
|
from models.model import AppMode, Conversation, Message, MessageAgentThought
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -389,6 +390,7 @@ class EasyUIBasedGenerateTaskPipeline(BasedGenerateTaskPipeline):
|
|||||||
if llm_result.message.content
|
if llm_result.message.content
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
message.updated_at = naive_utc_now()
|
||||||
message.answer_tokens = usage.completion_tokens
|
message.answer_tokens = usage.completion_tokens
|
||||||
message.answer_unit_price = usage.completion_unit_price
|
message.answer_unit_price = usage.completion_unit_price
|
||||||
message.answer_price_unit = usage.completion_price_unit
|
message.answer_price_unit = usage.completion_price_unit
|
||||||
|
|||||||
@ -276,17 +276,26 @@ class Executor:
|
|||||||
encoded_credentials = credentials
|
encoded_credentials = credentials
|
||||||
headers[authorization.config.header] = f"Basic {encoded_credentials}"
|
headers[authorization.config.header] = f"Basic {encoded_credentials}"
|
||||||
elif self.auth.config.type == "custom":
|
elif self.auth.config.type == "custom":
|
||||||
headers[authorization.config.header] = authorization.config.api_key or ""
|
if authorization.config.header and authorization.config.api_key:
|
||||||
|
headers[authorization.config.header] = authorization.config.api_key
|
||||||
|
|
||||||
# Handle Content-Type for multipart/form-data requests
|
# Handle Content-Type for multipart/form-data requests
|
||||||
# Fix for issue #22880: Missing boundary when using multipart/form-data
|
# Fix for issue #23829: Missing boundary when using multipart/form-data
|
||||||
body = self.node_data.body
|
body = self.node_data.body
|
||||||
if body and body.type == "form-data":
|
if body and body.type == "form-data":
|
||||||
# For multipart/form-data with files, let httpx handle the boundary automatically
|
# For multipart/form-data with files (including placeholder files),
|
||||||
# by not setting Content-Type header when files are present
|
# remove any manually set Content-Type header to let httpx handle
|
||||||
if not self.files or all(f[0] == "__multipart_placeholder__" for f in self.files):
|
# For multipart/form-data, if any files are present (including placeholder files),
|
||||||
# Only set Content-Type when there are no actual files
|
# we must remove any manually set Content-Type header. This is because httpx needs to
|
||||||
# This ensures httpx generates the correct boundary
|
# automatically set the Content-Type and boundary for multipart encoding whenever files
|
||||||
|
# are included, even if they are placeholders, to avoid boundary issues and ensure correct
|
||||||
|
# file upload behaviour. Manually setting Content-Type can cause httpx to fail to set the
|
||||||
|
# boundary, resulting in invalid requests.
|
||||||
|
if self.files:
|
||||||
|
# Remove Content-Type if it was manually set to avoid boundary issues
|
||||||
|
headers = {k: v for k, v in headers.items() if k.lower() != "content-type"}
|
||||||
|
else:
|
||||||
|
# No files at all, set Content-Type manually
|
||||||
if "content-type" not in (k.lower() for k in headers):
|
if "content-type" not in (k.lower() for k in headers):
|
||||||
headers["Content-Type"] = "multipart/form-data"
|
headers["Content-Type"] = "multipart/form-data"
|
||||||
elif body and body.type in BODY_TYPE_TO_CONTENT_TYPE:
|
elif body and body.type in BODY_TYPE_TO_CONTENT_TYPE:
|
||||||
|
|||||||
@ -249,6 +249,8 @@ def _get_remote_file_info(url: str):
|
|||||||
|
|
||||||
# Initialize mime_type from filename as fallback
|
# Initialize mime_type from filename as fallback
|
||||||
mime_type, _ = mimetypes.guess_type(filename)
|
mime_type, _ = mimetypes.guess_type(filename)
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type = ""
|
||||||
|
|
||||||
resp = ssrf_proxy.head(url, follow_redirects=True)
|
resp = ssrf_proxy.head(url, follow_redirects=True)
|
||||||
resp = cast(httpx.Response, resp)
|
resp = cast(httpx.Response, resp)
|
||||||
@ -257,7 +259,12 @@ def _get_remote_file_info(url: str):
|
|||||||
filename = str(content_disposition.split("filename=")[-1].strip('"'))
|
filename = str(content_disposition.split("filename=")[-1].strip('"'))
|
||||||
# Re-guess mime_type from updated filename
|
# Re-guess mime_type from updated filename
|
||||||
mime_type, _ = mimetypes.guess_type(filename)
|
mime_type, _ = mimetypes.guess_type(filename)
|
||||||
|
if mime_type is None:
|
||||||
|
mime_type = ""
|
||||||
file_size = int(resp.headers.get("Content-Length", file_size))
|
file_size = int(resp.headers.get("Content-Length", file_size))
|
||||||
|
# Fallback to Content-Type header if mime_type is still empty
|
||||||
|
if not mime_type:
|
||||||
|
mime_type = resp.headers.get("Content-Type", "").split(";")[0].strip()
|
||||||
|
|
||||||
return mime_type, filename, file_size
|
return mime_type, filename, file_size
|
||||||
|
|
||||||
|
|||||||
@ -160,6 +160,177 @@ def test_custom_authorization_header(setup_http_mock):
|
|||||||
|
|
||||||
assert "?A=b" in data
|
assert "?A=b" in data
|
||||||
assert "X-Header: 123" in data
|
assert "X-Header: 123" in data
|
||||||
|
# Custom authorization header should be set (may be masked)
|
||||||
|
assert "X-Auth:" in data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
|
def test_custom_auth_with_empty_api_key_does_not_set_header(setup_http_mock):
|
||||||
|
"""Test: In custom authentication mode, when the api_key is empty, no header should be set."""
|
||||||
|
from core.workflow.entities.variable_pool import VariablePool
|
||||||
|
from core.workflow.nodes.http_request.entities import (
|
||||||
|
HttpRequestNodeAuthorization,
|
||||||
|
HttpRequestNodeData,
|
||||||
|
HttpRequestNodeTimeout,
|
||||||
|
)
|
||||||
|
from core.workflow.nodes.http_request.executor import Executor
|
||||||
|
from core.workflow.system_variable import SystemVariable
|
||||||
|
|
||||||
|
# Create variable pool
|
||||||
|
variable_pool = VariablePool(
|
||||||
|
system_variables=SystemVariable(user_id="test", files=[]),
|
||||||
|
user_inputs={},
|
||||||
|
environment_variables=[],
|
||||||
|
conversation_variables=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create node data with custom auth and empty api_key
|
||||||
|
node_data = HttpRequestNodeData(
|
||||||
|
title="http",
|
||||||
|
desc="",
|
||||||
|
url="http://example.com",
|
||||||
|
method="get",
|
||||||
|
authorization=HttpRequestNodeAuthorization(
|
||||||
|
type="api-key",
|
||||||
|
config={
|
||||||
|
"type": "custom",
|
||||||
|
"api_key": "", # Empty api_key
|
||||||
|
"header": "X-Custom-Auth",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
headers="",
|
||||||
|
params="",
|
||||||
|
body=None,
|
||||||
|
ssl_verify=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create executor
|
||||||
|
executor = Executor(
|
||||||
|
node_data=node_data, timeout=HttpRequestNodeTimeout(connect=10, read=30, write=10), variable_pool=variable_pool
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get assembled headers
|
||||||
|
headers = executor._assembling_headers()
|
||||||
|
|
||||||
|
# When api_key is empty, the custom header should NOT be set
|
||||||
|
assert "X-Custom-Auth" not in headers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
|
def test_bearer_authorization_with_custom_header_ignored(setup_http_mock):
|
||||||
|
"""
|
||||||
|
Test that when switching from custom to bearer authorization,
|
||||||
|
the custom header settings don't interfere with bearer token.
|
||||||
|
This test verifies the fix for issue #23554.
|
||||||
|
"""
|
||||||
|
node = init_http_node(
|
||||||
|
config={
|
||||||
|
"id": "1",
|
||||||
|
"data": {
|
||||||
|
"title": "http",
|
||||||
|
"desc": "",
|
||||||
|
"method": "get",
|
||||||
|
"url": "http://example.com",
|
||||||
|
"authorization": {
|
||||||
|
"type": "api-key",
|
||||||
|
"config": {
|
||||||
|
"type": "bearer",
|
||||||
|
"api_key": "test-token",
|
||||||
|
"header": "", # Empty header - should default to Authorization
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"headers": "",
|
||||||
|
"params": "",
|
||||||
|
"body": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = node._run()
|
||||||
|
assert result.process_data is not None
|
||||||
|
data = result.process_data.get("request", "")
|
||||||
|
|
||||||
|
# In bearer mode, should use Authorization header (value is masked with *)
|
||||||
|
assert "Authorization: " in data
|
||||||
|
# Should contain masked Bearer token
|
||||||
|
assert "*" in data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
|
def test_basic_authorization_with_custom_header_ignored(setup_http_mock):
|
||||||
|
"""
|
||||||
|
Test that when switching from custom to basic authorization,
|
||||||
|
the custom header settings don't interfere with basic auth.
|
||||||
|
This test verifies the fix for issue #23554.
|
||||||
|
"""
|
||||||
|
node = init_http_node(
|
||||||
|
config={
|
||||||
|
"id": "1",
|
||||||
|
"data": {
|
||||||
|
"title": "http",
|
||||||
|
"desc": "",
|
||||||
|
"method": "get",
|
||||||
|
"url": "http://example.com",
|
||||||
|
"authorization": {
|
||||||
|
"type": "api-key",
|
||||||
|
"config": {
|
||||||
|
"type": "basic",
|
||||||
|
"api_key": "user:pass",
|
||||||
|
"header": "", # Empty header - should default to Authorization
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"headers": "",
|
||||||
|
"params": "",
|
||||||
|
"body": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = node._run()
|
||||||
|
assert result.process_data is not None
|
||||||
|
data = result.process_data.get("request", "")
|
||||||
|
|
||||||
|
# In basic mode, should use Authorization header (value is masked with *)
|
||||||
|
assert "Authorization: " in data
|
||||||
|
# Should contain masked Basic credentials
|
||||||
|
assert "*" in data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
|
def test_custom_authorization_with_empty_api_key(setup_http_mock):
|
||||||
|
"""
|
||||||
|
Test that custom authorization doesn't set header when api_key is empty.
|
||||||
|
This test verifies the fix for issue #23554.
|
||||||
|
"""
|
||||||
|
node = init_http_node(
|
||||||
|
config={
|
||||||
|
"id": "1",
|
||||||
|
"data": {
|
||||||
|
"title": "http",
|
||||||
|
"desc": "",
|
||||||
|
"method": "get",
|
||||||
|
"url": "http://example.com",
|
||||||
|
"authorization": {
|
||||||
|
"type": "api-key",
|
||||||
|
"config": {
|
||||||
|
"type": "custom",
|
||||||
|
"api_key": "", # Empty api_key
|
||||||
|
"header": "X-Custom-Auth",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"headers": "",
|
||||||
|
"params": "",
|
||||||
|
"body": None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = node._run()
|
||||||
|
assert result.process_data is not None
|
||||||
|
data = result.process_data.get("request", "")
|
||||||
|
|
||||||
|
# Custom header should NOT be set when api_key is empty
|
||||||
|
assert "X-Custom-Auth:" not in data
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
@ -239,6 +410,7 @@ def test_json(setup_http_mock):
|
|||||||
assert "X-Header: 123" in data
|
assert "X-Header: 123" in data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
def test_x_www_form_urlencoded(setup_http_mock):
|
def test_x_www_form_urlencoded(setup_http_mock):
|
||||||
node = init_http_node(
|
node = init_http_node(
|
||||||
config={
|
config={
|
||||||
@ -285,6 +457,7 @@ def test_x_www_form_urlencoded(setup_http_mock):
|
|||||||
assert "X-Header: 123" in data
|
assert "X-Header: 123" in data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
def test_form_data(setup_http_mock):
|
def test_form_data(setup_http_mock):
|
||||||
node = init_http_node(
|
node = init_http_node(
|
||||||
config={
|
config={
|
||||||
@ -334,6 +507,7 @@ def test_form_data(setup_http_mock):
|
|||||||
assert "X-Header: 123" in data
|
assert "X-Header: 123" in data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
def test_none_data(setup_http_mock):
|
def test_none_data(setup_http_mock):
|
||||||
node = init_http_node(
|
node = init_http_node(
|
||||||
config={
|
config={
|
||||||
@ -366,6 +540,7 @@ def test_none_data(setup_http_mock):
|
|||||||
assert "123123123" not in data
|
assert "123123123" not in data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
def test_mock_404(setup_http_mock):
|
def test_mock_404(setup_http_mock):
|
||||||
node = init_http_node(
|
node = init_http_node(
|
||||||
config={
|
config={
|
||||||
@ -394,6 +569,7 @@ def test_mock_404(setup_http_mock):
|
|||||||
assert "Not Found" in resp.get("body", "")
|
assert "Not Found" in resp.get("body", "")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("setup_http_mock", [["none"]], indirect=True)
|
||||||
def test_multi_colons_parse(setup_http_mock):
|
def test_multi_colons_parse(setup_http_mock):
|
||||||
node = init_http_node(
|
node = init_http_node(
|
||||||
config={
|
config={
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -4,8 +4,8 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
from werkzeug.exceptions import Forbidden
|
from werkzeug.exceptions import Forbidden
|
||||||
|
|
||||||
from controllers.common.errors import FilenameNotExistsError
|
from controllers.common.errors import (
|
||||||
from controllers.console.error import (
|
FilenameNotExistsError,
|
||||||
FileTooLargeError,
|
FileTooLargeError,
|
||||||
NoFileUploadedError,
|
NoFileUploadedError,
|
||||||
TooManyFilesError,
|
TooManyFilesError,
|
||||||
|
|||||||
@ -243,8 +243,6 @@ def test_executor_with_form_data():
|
|||||||
# Check the executor's data
|
# Check the executor's data
|
||||||
assert executor.method == "post"
|
assert executor.method == "post"
|
||||||
assert executor.url == "https://api.example.com/upload"
|
assert executor.url == "https://api.example.com/upload"
|
||||||
assert "Content-Type" in executor.headers
|
|
||||||
assert "multipart/form-data" in executor.headers["Content-Type"]
|
|
||||||
assert executor.params is None
|
assert executor.params is None
|
||||||
assert executor.json is None
|
assert executor.json is None
|
||||||
# '__multipart_placeholder__' is expected when no file inputs exist,
|
# '__multipart_placeholder__' is expected when no file inputs exist,
|
||||||
@ -252,6 +250,11 @@ def test_executor_with_form_data():
|
|||||||
assert executor.files == [("__multipart_placeholder__", ("", b"", "application/octet-stream"))]
|
assert executor.files == [("__multipart_placeholder__", ("", b"", "application/octet-stream"))]
|
||||||
assert executor.content is None
|
assert executor.content is None
|
||||||
|
|
||||||
|
# After fix for #23829: When placeholder files exist, Content-Type is removed
|
||||||
|
# to let httpx handle Content-Type and boundary automatically
|
||||||
|
headers = executor._assembling_headers()
|
||||||
|
assert "Content-Type" not in headers or "multipart/form-data" not in headers.get("Content-Type", "")
|
||||||
|
|
||||||
# Check that the form data is correctly loaded in executor.data
|
# Check that the form data is correctly loaded in executor.data
|
||||||
assert isinstance(executor.data, dict)
|
assert isinstance(executor.data, dict)
|
||||||
assert "text_field" in executor.data
|
assert "text_field" in executor.data
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import AppCard from '@/app/components/app/overview/appCard'
|
import AppCard from '@/app/components/app/overview/app-card'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
import MCPServiceCard from '@/app/components/tools/mcp/mcp-service-card'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
@ -17,7 +17,7 @@ import type { App } from '@/types/app'
|
|||||||
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
import type { UpdateAppSiteCodeResponse } from '@/models/app'
|
||||||
import { asyncRunSafe } from '@/utils'
|
import { asyncRunSafe } from '@/utils'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
|
import type { IAppCardProps } from '@/app/components/app/overview/app-card'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
|
|
||||||
export type ICardViewProps = {
|
export type ICardViewProps = {
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import React, { useState } from 'react'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { PeriodParams } from '@/app/components/app/overview/appChart'
|
import type { PeriodParams } from '@/app/components/app/overview/app-chart'
|
||||||
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/appChart'
|
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, MessagesChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/app-chart'
|
||||||
import type { Item } from '@/app/components/base/select'
|
import type { Item } from '@/app/components/base/select'
|
||||||
import { SimpleSelect } from '@/app/components/base/select'
|
import { SimpleSelect } from '@/app/components/base/select'
|
||||||
import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
|
import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
|
||||||
|
|||||||
@ -35,7 +35,7 @@ import type { AppDetailResponse } from '@/models/app'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import type { AppSSO } from '@/types/app'
|
import type { AppSSO } from '@/types/app'
|
||||||
import Indicator from '@/app/components/header/indicator'
|
import Indicator from '@/app/components/header/indicator'
|
||||||
import { fetchAppDetail } from '@/service/apps'
|
import { fetchAppDetailDirect } from '@/service/apps'
|
||||||
import { AccessMode } from '@/models/access-control'
|
import { AccessMode } from '@/models/access-control'
|
||||||
import AccessControl from '../app-access-control'
|
import AccessControl from '../app-access-control'
|
||||||
import { useAppWhiteListSubjects } from '@/service/access-control'
|
import { useAppWhiteListSubjects } from '@/service/access-control'
|
||||||
@ -161,11 +161,15 @@ function AppCard({
|
|||||||
return
|
return
|
||||||
setShowAccessControl(true)
|
setShowAccessControl(true)
|
||||||
}, [appDetail])
|
}, [appDetail])
|
||||||
const handleAccessControlUpdate = useCallback(() => {
|
const handleAccessControlUpdate = useCallback(async () => {
|
||||||
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
|
try {
|
||||||
|
const res = await fetchAppDetailDirect({ url: '/apps', id: appDetail!.id })
|
||||||
setAppDetail(res)
|
setAppDetail(res)
|
||||||
setShowAccessControl(false)
|
setShowAccessControl(false)
|
||||||
})
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Failed to fetch app detail:', error)
|
||||||
|
}
|
||||||
}, [appDetail, setAppDetail])
|
}, [appDetail, setAppDetail])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -123,7 +123,7 @@ const Chart: React.FC<IChartProps> = ({
|
|||||||
dimensions: ['date', yField],
|
dimensions: ['date', yField],
|
||||||
source: statistics,
|
source: statistics,
|
||||||
},
|
},
|
||||||
grid: { top: 8, right: 36, bottom: 0, left: 0, containLabel: true },
|
grid: { top: 8, right: 36, bottom: 10, left: 25, containLabel: true },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
position: 'top',
|
position: 'top',
|
||||||
@ -165,7 +165,7 @@ const Chart: React.FC<IChartProps> = ({
|
|||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: COMMON_COLOR_MAP.splitLineDark,
|
color: COMMON_COLOR_MAP.splitLineDark,
|
||||||
},
|
},
|
||||||
interval(index, value) {
|
interval(_index, value) {
|
||||||
return !!value
|
return !!value
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -242,7 +242,7 @@ const Chart: React.FC<IChartProps> = ({
|
|||||||
? ''
|
? ''
|
||||||
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
|
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
|
||||||
<span className='ml-1 text-text-tertiary'>(</span>
|
<span className='ml-1 text-text-tertiary'>(</span>
|
||||||
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
|
<span className='text-orange-400'>~{sum(statistics.map(item => Number.parseFloat(String(get(item, 'total_price', '0'))))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
|
||||||
<span className='text-text-tertiary'>)</span>
|
<span className='text-text-tertiary'>)</span>
|
||||||
</span></span>}
|
</span></span>}
|
||||||
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-text-quaternary' : ''}` }} />
|
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-text-quaternary' : ''}` }} />
|
||||||
@ -268,7 +268,7 @@ export const MessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
|
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
|
||||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||||
chartType='messages'
|
chartType='messages'
|
||||||
{...(noDataFlag && { yMax: 500 })}
|
{...(noDataFlag && { yMax: 500 })}
|
||||||
/>
|
/>
|
||||||
@ -282,7 +282,7 @@ export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
basicInfo={{ title: t('appOverview.analysis.totalConversations.title'), explanation: t('appOverview.analysis.totalConversations.explanation'), timePeriod: period.name }}
|
basicInfo={{ title: t('appOverview.analysis.totalConversations.title'), explanation: t('appOverview.analysis.totalConversations.explanation'), timePeriod: period.name }}
|
||||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||||
chartType='conversations'
|
chartType='conversations'
|
||||||
{...(noDataFlag && { yMax: 500 })}
|
{...(noDataFlag && { yMax: 500 })}
|
||||||
/>
|
/>
|
||||||
@ -297,7 +297,7 @@ export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
|
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
|
||||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||||
chartType='endUsers'
|
chartType='endUsers'
|
||||||
{...(noDataFlag && { yMax: 500 })}
|
{...(noDataFlag && { yMax: 500 })}
|
||||||
/>
|
/>
|
||||||
@ -380,7 +380,7 @@ export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
|
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
|
||||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||||
chartType='costs'
|
chartType='costs'
|
||||||
{...(noDataFlag && { yMax: 100 })}
|
{...(noDataFlag && { yMax: 100 })}
|
||||||
/>
|
/>
|
||||||
@ -394,7 +394,7 @@ export const WorkflowMessagesChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
|
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
|
||||||
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...(period.query ?? defaultPeriod), key: 'runs' }) }}
|
chartData={!noDataFlag ? response : { data: getDefaultChartData({ ...(period.query ?? defaultPeriod), key: 'runs' }) } as any}
|
||||||
chartType='conversations'
|
chartType='conversations'
|
||||||
valueKey='runs'
|
valueKey='runs'
|
||||||
{...(noDataFlag && { yMax: 500 })}
|
{...(noDataFlag && { yMax: 500 })}
|
||||||
@ -410,7 +410,7 @@ export const WorkflowDailyTerminalsChart: FC<IBizChartProps> = ({ id, period })
|
|||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
|
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
|
||||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||||
chartType='endUsers'
|
chartType='endUsers'
|
||||||
{...(noDataFlag && { yMax: 500 })}
|
{...(noDataFlag && { yMax: 500 })}
|
||||||
/>
|
/>
|
||||||
@ -425,7 +425,7 @@ export const WorkflowCostChart: FC<IBizChartProps> = ({ id, period }) => {
|
|||||||
const noDataFlag = !response.data || response.data.length === 0
|
const noDataFlag = !response.data || response.data.length === 0
|
||||||
return <Chart
|
return <Chart
|
||||||
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
|
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
|
||||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) }}
|
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query ?? defaultPeriod) } as any}
|
||||||
chartType='workflowCosts'
|
chartType='workflowCosts'
|
||||||
{...(noDataFlag && { yMax: 100 })}
|
{...(noDataFlag && { yMax: 100 })}
|
||||||
/>
|
/>
|
||||||
@ -18,3 +18,13 @@
|
|||||||
.pluginInstallIcon {
|
.pluginInstallIcon {
|
||||||
background-image: url(../assets/chromeplugin-install.svg);
|
background-image: url(../assets/chromeplugin-install.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme="dark"]) .iframeIcon,
|
||||||
|
:global(html[data-theme="dark"]) .scriptsIcon,
|
||||||
|
:global(html[data-theme="dark"]) .chromePluginIcon {
|
||||||
|
filter: invert(0.86) hue-rotate(180deg) saturate(0.5) brightness(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html[data-theme="dark"]) .pluginInstallIcon {
|
||||||
|
filter: invert(0.9);
|
||||||
|
}
|
||||||
|
|||||||
@ -60,6 +60,7 @@
|
|||||||
@apply
|
@apply
|
||||||
border-[0.5px]
|
border-[0.5px]
|
||||||
shadow-xs
|
shadow-xs
|
||||||
|
backdrop-blur-[5px]
|
||||||
bg-components-button-secondary-bg
|
bg-components-button-secondary-bg
|
||||||
border-components-button-secondary-border
|
border-components-button-secondary-border
|
||||||
hover:bg-components-button-secondary-bg-hover
|
hover:bg-components-button-secondary-bg-hover
|
||||||
@ -69,6 +70,7 @@
|
|||||||
|
|
||||||
.btn-secondary.btn-disabled {
|
.btn-secondary.btn-disabled {
|
||||||
@apply
|
@apply
|
||||||
|
backdrop-blur-sm
|
||||||
bg-components-button-secondary-bg-disabled
|
bg-components-button-secondary-bg-disabled
|
||||||
border-components-button-secondary-border-disabled
|
border-components-button-secondary-border-disabled
|
||||||
text-components-button-secondary-text-disabled;
|
text-components-button-secondary-text-disabled;
|
||||||
|
|||||||
@ -117,7 +117,7 @@ const Flowchart = React.forwardRef((props: {
|
|||||||
const [isInitialized, setIsInitialized] = useState(false)
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>(props.theme || 'light')
|
const [currentTheme, setCurrentTheme] = useState<'light' | 'dark'>(props.theme || 'light')
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const chartId = useRef(`mermaid-chart-${Math.random().toString(36).substr(2, 9)}`).current
|
const chartId = useRef(`mermaid-chart-${Math.random().toString(36).slice(2, 11)}`).current
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const renderTimeoutRef = useRef<NodeJS.Timeout>()
|
const renderTimeoutRef = useRef<NodeJS.Timeout>()
|
||||||
const [errMsg, setErrMsg] = useState('')
|
const [errMsg, setErrMsg] = useState('')
|
||||||
|
|||||||
@ -259,7 +259,7 @@ function getFullMatchOffset(
|
|||||||
): number {
|
): number {
|
||||||
let triggerOffset = offset
|
let triggerOffset = offset
|
||||||
for (let i = triggerOffset; i <= entryText.length; i++) {
|
for (let i = triggerOffset; i <= entryText.length; i++) {
|
||||||
if (documentText.substr(-i) === entryText.substr(0, i))
|
if (documentText.slice(-i) === entryText.slice(0, i))
|
||||||
triggerOffset = i
|
triggerOffset = i
|
||||||
}
|
}
|
||||||
return triggerOffset
|
return triggerOffset
|
||||||
|
|||||||
42
web/app/components/billing/pricing/header.tsx
Normal file
42
web/app/components/billing/pricing/header.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import DifyLogo from '../../base/logo/dify-logo'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Button from '../../base/button'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header = ({
|
||||||
|
onClose,
|
||||||
|
}: HeaderProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex min-h-[105px] w-full justify-center px-10'>
|
||||||
|
<div className='relative flex max-w-[1680px] grow flex-col justify-end gap-y-1 border-x border-divider-accent p-6 pt-8'>
|
||||||
|
<div className='flex items-end'>
|
||||||
|
<div className='py-[5px]'>
|
||||||
|
<DifyLogo className='h-[27px] w-[60px]' />
|
||||||
|
</div>
|
||||||
|
<span className='overflow-visible bg-billing-plan-title-bg bg-clip-text px-1.5 font-instrument text-[37px] italic leading-[1.2] text-transparent'>
|
||||||
|
{t('billing.plansCommon.title.plans')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className='system-sm-regular text-text-tertiary'>
|
||||||
|
{t('billing.plansCommon.title.description')}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant='secondary'
|
||||||
|
className='absolute bottom-[40.5px] right-[-18px] z-10 size-9 rounded-full p-2'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<RiCloseLine className='size-5' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Header)
|
||||||
@ -1,51 +1,63 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import Header from './header'
|
||||||
import { RiArrowRightUpLine, RiCloseLine, RiCloudFill, RiTerminalBoxFill } from '@remixicon/react'
|
import PlanSwitcher from './plan-switcher'
|
||||||
import Link from 'next/link'
|
import { PlanRange } from './plan-switcher/plan-range-switcher'
|
||||||
import { useKeyPress } from 'ahooks'
|
import { useKeyPress } from 'ahooks'
|
||||||
import { Plan, SelfHostedPlan } from '../type'
|
// import { useTranslation } from 'react-i18next'
|
||||||
import TabSlider from '../../base/tab-slider'
|
// import { RiArrowRightUpLine, RiCloseLine, RiCloudFill, RiTerminalBoxFill } from '@remixicon/react'
|
||||||
import SelectPlanRange, { PlanRange } from './select-plan-range'
|
// import Link from 'next/link'
|
||||||
import PlanItem from './plan-item'
|
// import { Plan, SelfHostedPlan } from '../type'
|
||||||
import SelfHostedPlanItem from './self-hosted-plan-item'
|
// import TabSlider from '../../base/tab-slider'
|
||||||
import { useProviderContext } from '@/context/provider-context'
|
// import PlanItem from './plan-item'
|
||||||
import GridMask from '@/app/components/base/grid-mask'
|
// import SelfHostedPlanItem from './self-hosted-plan-item'
|
||||||
import { useAppContext } from '@/context/app-context'
|
// import { useProviderContext } from '@/context/provider-context'
|
||||||
import classNames from '@/utils/classnames'
|
// import GridMask from '@/app/components/base/grid-mask'
|
||||||
import { useGetPricingPageLanguage } from '@/context/i18n'
|
// import { useAppContext } from '@/context/app-context'
|
||||||
|
// import classNames from '@/utils/classnames'
|
||||||
|
// import { useGetPricingPageLanguage } from '@/context/i18n'
|
||||||
|
|
||||||
type Props = {
|
export type Category = 'cloud' | 'self'
|
||||||
|
|
||||||
|
type PricingProps = {
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Pricing: FC<Props> = ({
|
const Pricing: FC<PricingProps> = ({
|
||||||
onCancel,
|
onCancel,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
// const { t } = useTranslation()
|
||||||
const { plan } = useProviderContext()
|
// const { plan } = useProviderContext()
|
||||||
const { isCurrentWorkspaceManager } = useAppContext()
|
// const { isCurrentWorkspaceManager } = useAppContext()
|
||||||
const canPay = isCurrentWorkspaceManager
|
// const canPay = isCurrentWorkspaceManager
|
||||||
const [planRange, setPlanRange] = React.useState<PlanRange>(PlanRange.monthly)
|
const [planRange, setPlanRange] = React.useState<PlanRange>(PlanRange.monthly)
|
||||||
|
|
||||||
const [currentPlan, setCurrentPlan] = React.useState<string>('cloud')
|
const [currentCategory, setCurrentCategory] = useState<Category>('cloud')
|
||||||
|
|
||||||
useKeyPress(['esc'], onCancel)
|
useKeyPress(['esc'], onCancel)
|
||||||
|
|
||||||
const pricingPageLanguage = useGetPricingPageLanguage()
|
// const pricingPageLanguage = useGetPricingPageLanguage()
|
||||||
const pricingPageURL = pricingPageLanguage
|
// const pricingPageURL = pricingPageLanguage
|
||||||
? `https://dify.ai/${pricingPageLanguage}/pricing#plans-and-features`
|
// ? `https://dify.ai/${pricingPageLanguage}/pricing#plans-and-features`
|
||||||
: 'https://dify.ai/pricing#plans-and-features'
|
// : 'https://dify.ai/pricing#plans-and-features'
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className='fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] bg-background-overlay-backdrop p-4 backdrop-blur-[6px]'
|
className='fixed inset-0 bottom-0 left-0 right-0 top-0 z-[1000] overflow-auto bg-saas-background'
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className='relative h-full w-full overflow-auto rounded-2xl border border-effects-highlight bg-saas-background'>
|
<div className='relative h-full min-w-[1200px]'>
|
||||||
<div
|
<Header onClose={onCancel} />
|
||||||
|
<PlanSwitcher
|
||||||
|
currentCategory={currentCategory}
|
||||||
|
onChangeCategory={setCurrentCategory}
|
||||||
|
currentPlanRange={planRange}
|
||||||
|
onChangePlanRange={setPlanRange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <div
|
||||||
className='fixed right-7 top-7 z-[1001] flex h-9 w-9 cursor-pointer items-center justify-center rounded-[10px] bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover'
|
className='fixed right-7 top-7 z-[1001] flex h-9 w-9 cursor-pointer items-center justify-center rounded-[10px] bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover'
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
@ -137,8 +149,8 @@ const Pricing: FC<Props> = ({
|
|||||||
<RiArrowRightUpLine className='size-4' />
|
<RiArrowRightUpLine className='size-4' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridMask>
|
</GridMask> */}
|
||||||
</div >
|
</div>
|
||||||
</div >,
|
</div >,
|
||||||
document.body,
|
document.body,
|
||||||
)
|
)
|
||||||
|
|||||||
64
web/app/components/billing/pricing/plan-switcher/index.tsx
Normal file
64
web/app/components/billing/pricing/plan-switcher/index.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import type { Category } from '../index'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Ri24HoursFill, RiTerminalBoxFill } from '@remixicon/react'
|
||||||
|
import Tab from './tab'
|
||||||
|
import Divider from '@/app/components/base/divider'
|
||||||
|
import type { PlanRange } from './plan-range-switcher'
|
||||||
|
import PlanRangeSwitcher from './plan-range-switcher'
|
||||||
|
|
||||||
|
type PlanSwitcherProps = {
|
||||||
|
currentCategory: Category
|
||||||
|
currentPlanRange: PlanRange
|
||||||
|
onChangeCategory: (category: Category) => void
|
||||||
|
onChangePlanRange: (value: PlanRange) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanSwitcher: FC<PlanSwitcherProps> = ({
|
||||||
|
currentCategory,
|
||||||
|
currentPlanRange,
|
||||||
|
onChangeCategory,
|
||||||
|
onChangePlanRange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const tabs = {
|
||||||
|
cloud: {
|
||||||
|
value: 'cloud' as Category,
|
||||||
|
label: t('billing.plansCommon.cloud'),
|
||||||
|
Icon: Ri24HoursFill,
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
value: 'self' as Category,
|
||||||
|
label: t('billing.plansCommon.self'),
|
||||||
|
Icon: RiTerminalBoxFill,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex w-full justify-center border-t border-divider-accent px-10'>
|
||||||
|
<div className='flex max-w-[1680px] grow items-center justify-between border-x border-divider-accent p-1'>
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<Tab<Category>
|
||||||
|
{...tabs.cloud}
|
||||||
|
isActive={currentCategory === tabs.cloud.value}
|
||||||
|
onClick={onChangeCategory}
|
||||||
|
/>
|
||||||
|
<Divider type='vertical' className='mx-2 h-4 bg-divider-accent' />
|
||||||
|
<Tab<Category>
|
||||||
|
{...tabs.self}
|
||||||
|
isActive={currentCategory === tabs.self.value}
|
||||||
|
onClick={onChangeCategory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PlanRangeSwitcher
|
||||||
|
value={currentPlanRange}
|
||||||
|
onChange={onChangePlanRange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(PlanSwitcher)
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Switch from '../../../base/switch'
|
||||||
|
|
||||||
|
export enum PlanRange {
|
||||||
|
monthly = 'monthly',
|
||||||
|
yearly = 'yearly',
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlanRangeSwitcherProps = {
|
||||||
|
value: PlanRange
|
||||||
|
onChange: (value: PlanRange) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanRangeSwitcher: FC<PlanRangeSwitcherProps> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex items-center justify-end gap-x-3 pr-5'>
|
||||||
|
<Switch
|
||||||
|
size='l'
|
||||||
|
defaultValue={value === PlanRange.yearly}
|
||||||
|
onChange={(v) => {
|
||||||
|
onChange(v ? PlanRange.yearly : PlanRange.monthly)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className='system-md-regular text-text-tertiary'>
|
||||||
|
{t('billing.plansCommon.annualBilling', { percent: 17 })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(PlanRangeSwitcher)
|
||||||
37
web/app/components/billing/pricing/plan-switcher/tab.tsx
Normal file
37
web/app/components/billing/pricing/plan-switcher/tab.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
|
type TabProps<T> = {
|
||||||
|
Icon: React.ComponentType<any>
|
||||||
|
value: T
|
||||||
|
label: string
|
||||||
|
isActive: boolean
|
||||||
|
onClick: (value: T) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tab = <T,>({
|
||||||
|
Icon,
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
isActive,
|
||||||
|
onClick,
|
||||||
|
}: TabProps<T>) => {
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onClick(value)
|
||||||
|
}, [onClick, value])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex cursor-pointer items-center justify-center gap-x-2 px-5 py-3 text-text-secondary',
|
||||||
|
isActive && 'text-saas-dify-blue-accessible',
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<Icon className='size-4' />
|
||||||
|
<span className='system-xl-semibold'>{label}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Tab) as typeof Tab
|
||||||
@ -32,7 +32,7 @@ const GotoAnything: FC<Props> = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [show, setShow] = useState<boolean>(false)
|
const [show, setShow] = useState<boolean>(false)
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||||
const [cmdVal, setCmdVal] = useState<string>('')
|
const [cmdVal, setCmdVal] = useState<string>('_')
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const handleNavSearch = useCallback((q: string) => {
|
const handleNavSearch = useCallback((q: string) => {
|
||||||
setShow(true)
|
setShow(true)
|
||||||
@ -120,9 +120,14 @@ const GotoAnything: FC<Props> = ({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Prevent automatic selection of the first option when cmdVal is not set
|
||||||
|
const clearSelection = () => {
|
||||||
|
setCmdVal('_')
|
||||||
|
}
|
||||||
|
|
||||||
const handleCommandSelect = useCallback((commandKey: string) => {
|
const handleCommandSelect = useCallback((commandKey: string) => {
|
||||||
setSearchQuery(`${commandKey} `)
|
setSearchQuery(`${commandKey} `)
|
||||||
setCmdVal('')
|
clearSelection()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
}, 0)
|
}, 0)
|
||||||
@ -233,9 +238,6 @@ const GotoAnything: FC<Props> = ({
|
|||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
setCmdVal('')
|
|
||||||
}
|
|
||||||
}, [show])
|
}, [show])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -245,6 +247,7 @@ const GotoAnything: FC<Props> = ({
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShow(false)
|
setShow(false)
|
||||||
setSearchQuery('')
|
setSearchQuery('')
|
||||||
|
clearSelection()
|
||||||
onHide?.()
|
onHide?.()
|
||||||
}}
|
}}
|
||||||
closable={false}
|
closable={false}
|
||||||
@ -268,7 +271,7 @@ const GotoAnything: FC<Props> = ({
|
|||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearchQuery(e.target.value)
|
setSearchQuery(e.target.value)
|
||||||
if (!e.target.value.startsWith('@'))
|
if (!e.target.value.startsWith('@'))
|
||||||
setCmdVal('')
|
clearSelection()
|
||||||
}}
|
}}
|
||||||
className='flex-1 !border-0 !bg-transparent !shadow-none'
|
className='flex-1 !border-0 !bg-transparent !shadow-none'
|
||||||
wrapperClassName='flex-1 !border-0 !bg-transparent'
|
wrapperClassName='flex-1 !border-0 !bg-transparent'
|
||||||
@ -321,40 +324,40 @@ const GotoAnything: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
Object.entries(groupedResults).map(([type, results], groupIndex) => (
|
Object.entries(groupedResults).map(([type, results], groupIndex) => (
|
||||||
<Command.Group key={groupIndex} heading={(() => {
|
<Command.Group key={groupIndex} heading={(() => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
'app': 'app.gotoAnything.groups.apps',
|
'app': 'app.gotoAnything.groups.apps',
|
||||||
'plugin': 'app.gotoAnything.groups.plugins',
|
'plugin': 'app.gotoAnything.groups.plugins',
|
||||||
'knowledge': 'app.gotoAnything.groups.knowledgeBases',
|
'knowledge': 'app.gotoAnything.groups.knowledgeBases',
|
||||||
'workflow-node': 'app.gotoAnything.groups.workflowNodes',
|
'workflow-node': 'app.gotoAnything.groups.workflowNodes',
|
||||||
}
|
}
|
||||||
return t(typeMap[type] || `${type}s`)
|
return t(typeMap[type] || `${type}s`)
|
||||||
})()} className='p-2 capitalize text-text-secondary'>
|
})()} className='p-2 capitalize text-text-secondary'>
|
||||||
{results.map(result => (
|
{results.map(result => (
|
||||||
<Command.Item
|
<Command.Item
|
||||||
key={`${result.type}-${result.id}`}
|
key={`${result.type}-${result.id}`}
|
||||||
value={result.title}
|
value={result.title}
|
||||||
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover-alt data-[selected=true]:bg-state-base-hover-alt'
|
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] aria-[selected=true]:bg-state-base-hover data-[selected=true]:bg-state-base-hover'
|
||||||
onSelect={() => handleNavigate(result)}
|
onSelect={() => handleNavigate(result)}
|
||||||
>
|
>
|
||||||
{result.icon}
|
{result.icon}
|
||||||
<div className='min-w-0 flex-1'>
|
<div className='min-w-0 flex-1'>
|
||||||
<div className='truncate font-medium text-text-secondary'>
|
<div className='truncate font-medium text-text-secondary'>
|
||||||
{result.title}
|
{result.title}
|
||||||
</div>
|
|
||||||
{result.description && (
|
|
||||||
<div className='mt-0.5 truncate text-xs text-text-quaternary'>
|
|
||||||
{result.description}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{result.description && (
|
||||||
</div>
|
<div className='mt-0.5 truncate text-xs text-text-quaternary'>
|
||||||
<div className='text-xs capitalize text-text-quaternary'>
|
{result.description}
|
||||||
{result.type}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</Command.Item>
|
</div>
|
||||||
))}
|
<div className='text-xs capitalize text-text-quaternary'>
|
||||||
</Command.Group>
|
{result.type}
|
||||||
))
|
</div>
|
||||||
|
</Command.Item>
|
||||||
|
))}
|
||||||
|
</Command.Group>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
{!isCommandsMode && emptyResult}
|
{!isCommandsMode && emptyResult}
|
||||||
{!isCommandsMode && defaultUI}
|
{!isCommandsMode && defaultUI}
|
||||||
@ -373,7 +376,7 @@ const GotoAnything: FC<Props> = ({
|
|||||||
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
|
{t('app.gotoAnything.resultCount', { count: searchResults.length })}
|
||||||
{searchMode !== 'general' && (
|
{searchMode !== 'general' && (
|
||||||
<span className='ml-2 opacity-60'>
|
<span className='ml-2 opacity-60'>
|
||||||
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
|
{t('app.gotoAnything.inScope', { scope: searchMode.replace('@', '') })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import './styles/globals.css'
|
|||||||
import './styles/markdown.scss'
|
import './styles/markdown.scss'
|
||||||
import GlobalPublicStoreProvider from '@/context/global-public-context'
|
import GlobalPublicStoreProvider from '@/context/global-public-context'
|
||||||
import { DatasetAttr } from '@/types/feature'
|
import { DatasetAttr } from '@/types/feature'
|
||||||
|
import { Instrument_Serif } from 'next/font/google'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
width: 'device-width',
|
width: 'device-width',
|
||||||
@ -19,6 +21,13 @@ export const viewport: Viewport = {
|
|||||||
userScalable: false,
|
userScalable: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const instrumentSerif = Instrument_Serif({
|
||||||
|
weight: ['400'],
|
||||||
|
style: ['normal', 'italic'],
|
||||||
|
subsets: ['latin'],
|
||||||
|
variable: '--font-instrument-serif',
|
||||||
|
})
|
||||||
|
|
||||||
const LocaleLayout = async ({
|
const LocaleLayout = async ({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@ -51,15 +60,15 @@ const LocaleLayout = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale ?? 'en'} className="h-full" suppressHydrationWarning>
|
<html lang={locale ?? 'en'} className={cn('h-full', instrumentSerif.variable)} suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta name="theme-color" content="#FFFFFF" />
|
<meta name='theme-color' content='#FFFFFF' />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name='mobile-web-app-capable' content='yes' />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
<meta name='apple-mobile-web-app-status-bar-style' content='default' />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
className="color-scheme h-full select-auto"
|
className='color-scheme h-full select-auto'
|
||||||
{...datasetMap}
|
{...datasetMap}
|
||||||
>
|
>
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export default function RoutePrefixHandle() {
|
|||||||
const handleRouteChange = () => {
|
const handleRouteChange = () => {
|
||||||
const addPrefixToImg = (e: HTMLImageElement) => {
|
const addPrefixToImg = (e: HTMLImageElement) => {
|
||||||
const url = new URL(e.src)
|
const url = new URL(e.src)
|
||||||
const prefix = url.pathname.substr(0, basePath.length)
|
const prefix = url.pathname.slice(0, basePath.length)
|
||||||
if (prefix !== basePath && !url.href.startsWith('blob:') && !url.href.startsWith('data:')) {
|
if (prefix !== basePath && !url.href.startsWith('blob:') && !url.href.startsWith('data:')) {
|
||||||
url.pathname = basePath + url.pathname
|
url.pathname = basePath + url.pathname
|
||||||
e.src = url.toString()
|
e.src = url.toString()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useFavicon, useTitle } from 'ahooks'
|
import { useFavicon, useTitle } from 'ahooks'
|
||||||
|
import { basePath } from '@/utils/var'
|
||||||
|
|
||||||
export default function useDocumentTitle(title: string) {
|
export default function useDocumentTitle(title: string) {
|
||||||
const isPending = useGlobalPublicStore(s => s.isGlobalPending)
|
const isPending = useGlobalPublicStore(s => s.isGlobalPending)
|
||||||
@ -15,7 +16,7 @@ export default function useDocumentTitle(title: string) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
titleStr = `${prefix}Dify`
|
titleStr = `${prefix}Dify`
|
||||||
favicon = '/favicon.ico'
|
favicon = `${basePath}/favicon.ico`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
useTitle(titleStr)
|
useTitle(titleStr)
|
||||||
|
|||||||
@ -256,11 +256,11 @@ const translation = {
|
|||||||
maxActiveRequestsTip: 'Maximale Anzahl gleichzeitiger aktiver Anfragen pro App (0 für unbegrenzt)',
|
maxActiveRequestsTip: 'Maximale Anzahl gleichzeitiger aktiver Anfragen pro App (0 für unbegrenzt)',
|
||||||
gotoAnything: {
|
gotoAnything: {
|
||||||
actions: {
|
actions: {
|
||||||
searchPlugins: 'Such-Plugins',
|
searchPlugins: 'Plugins durchsuchen',
|
||||||
searchKnowledgeBases: 'Wissensdatenbanken durchsuchen',
|
searchKnowledgeBases: 'Wissensdatenbanken durchsuchen',
|
||||||
searchWorkflowNodes: 'Workflow-Knoten durchsuchen',
|
searchWorkflowNodes: 'Workflow-Knoten durchsuchen',
|
||||||
searchKnowledgeBasesDesc: 'Suchen und navigieren Sie zu Ihren Wissensdatenbanken',
|
searchKnowledgeBasesDesc: 'Suchen und navigieren Sie zu Ihren Wissensdatenbanken',
|
||||||
searchApplications: 'Anwendungen suchen',
|
searchApplications: 'Anwendungen durchsuchen',
|
||||||
searchWorkflowNodesHelp: 'Diese Funktion funktioniert nur, wenn ein Workflow angezeigt wird. Navigieren Sie zuerst zu einem Workflow.',
|
searchWorkflowNodesHelp: 'Diese Funktion funktioniert nur, wenn ein Workflow angezeigt wird. Navigieren Sie zuerst zu einem Workflow.',
|
||||||
searchApplicationsDesc: 'Suchen und navigieren Sie zu Ihren Anwendungen',
|
searchApplicationsDesc: 'Suchen und navigieren Sie zu Ihren Anwendungen',
|
||||||
searchPluginsDesc: 'Suchen und navigieren Sie zu Ihren Plugins',
|
searchPluginsDesc: 'Suchen und navigieren Sie zu Ihren Plugins',
|
||||||
|
|||||||
@ -17,7 +17,10 @@ const translation = {
|
|||||||
viewBilling: 'Manage billing and subscriptions',
|
viewBilling: 'Manage billing and subscriptions',
|
||||||
buyPermissionDeniedTip: 'Please contact your enterprise administrator to subscribe',
|
buyPermissionDeniedTip: 'Please contact your enterprise administrator to subscribe',
|
||||||
plansCommon: {
|
plansCommon: {
|
||||||
title: 'Pricing that powers your AI journey',
|
title: {
|
||||||
|
plans: 'plans',
|
||||||
|
description: 'Select the plan that best fits your team\'s needs.',
|
||||||
|
},
|
||||||
freeTrialTipPrefix: 'Sign up and get a ',
|
freeTrialTipPrefix: 'Sign up and get a ',
|
||||||
freeTrialTip: 'free trial of 200 OpenAI calls. ',
|
freeTrialTip: 'free trial of 200 OpenAI calls. ',
|
||||||
freeTrialTipSuffix: 'No credit card required',
|
freeTrialTipSuffix: 'No credit card required',
|
||||||
@ -33,7 +36,7 @@ const translation = {
|
|||||||
year: 'year',
|
year: 'year',
|
||||||
save: 'Save ',
|
save: 'Save ',
|
||||||
free: 'Free',
|
free: 'Free',
|
||||||
annualBilling: 'Annual Billing',
|
annualBilling: 'Bill Annually Save {{percent}}%',
|
||||||
comparePlanAndFeatures: 'Compare plans & features',
|
comparePlanAndFeatures: 'Compare plans & features',
|
||||||
priceTip: 'per workspace/',
|
priceTip: 'per workspace/',
|
||||||
currentPlan: 'Current Plan',
|
currentPlan: 'Current Plan',
|
||||||
|
|||||||
@ -254,10 +254,10 @@ const translation = {
|
|||||||
maxActiveRequestsTip: 'Número máximo de solicitudes activas concurrentes por aplicación (0 para ilimitado)',
|
maxActiveRequestsTip: 'Número máximo de solicitudes activas concurrentes por aplicación (0 para ilimitado)',
|
||||||
gotoAnything: {
|
gotoAnything: {
|
||||||
actions: {
|
actions: {
|
||||||
searchApplications: 'Aplicaciones de búsqueda',
|
searchApplications: 'Buscar aplicaciones',
|
||||||
searchKnowledgeBasesDesc: 'Busque y navegue por sus bases de conocimiento',
|
searchKnowledgeBasesDesc: 'Busque y navegue por sus bases de conocimiento',
|
||||||
searchWorkflowNodes: 'Buscar nodos de flujo de trabajo',
|
searchWorkflowNodes: 'Buscar nodos de flujo de trabajo',
|
||||||
searchPlugins: 'Complementos de búsqueda',
|
searchPlugins: 'Buscar complementos',
|
||||||
searchWorkflowNodesDesc: 'Buscar y saltar a nodos en el flujo de trabajo actual por nombre o tipo',
|
searchWorkflowNodesDesc: 'Buscar y saltar a nodos en el flujo de trabajo actual por nombre o tipo',
|
||||||
searchKnowledgeBases: 'Buscar en las bases de conocimiento',
|
searchKnowledgeBases: 'Buscar en las bases de conocimiento',
|
||||||
searchApplicationsDesc: 'Buscar y navegar a sus aplicaciones',
|
searchApplicationsDesc: 'Buscar y navegar a sus aplicaciones',
|
||||||
|
|||||||
@ -254,8 +254,8 @@ const translation = {
|
|||||||
maxActiveRequestsTip: 'حداکثر تعداد درخواستهای فعال همزمان در هر برنامه (0 برای نامحدود)',
|
maxActiveRequestsTip: 'حداکثر تعداد درخواستهای فعال همزمان در هر برنامه (0 برای نامحدود)',
|
||||||
gotoAnything: {
|
gotoAnything: {
|
||||||
actions: {
|
actions: {
|
||||||
searchPlugins: 'افزونه های جستجو',
|
searchPlugins: 'جستجوی افزونه ها',
|
||||||
searchWorkflowNodes: 'گره های گردش کار جستجو',
|
searchWorkflowNodes: 'جستجوی گره های گردش کار',
|
||||||
searchApplications: 'جستجوی برنامه ها',
|
searchApplications: 'جستجوی برنامه ها',
|
||||||
searchKnowledgeBases: 'جستجو در پایگاه های دانش',
|
searchKnowledgeBases: 'جستجو در پایگاه های دانش',
|
||||||
searchWorkflowNodesHelp: 'این ویژگی فقط هنگام مشاهده گردش کار کار می کند. ابتدا به گردش کار بروید.',
|
searchWorkflowNodesHelp: 'این ویژگی فقط هنگام مشاهده گردش کار کار می کند. ابتدا به گردش کار بروید.',
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const translation = {
|
|||||||
appCreateDSLErrorTitle: 'Incompatibilité de version',
|
appCreateDSLErrorTitle: 'Incompatibilité de version',
|
||||||
appCreateDSLErrorPart3: 'Version actuelle de l’application DSL :',
|
appCreateDSLErrorPart3: 'Version actuelle de l’application DSL :',
|
||||||
appCreateDSLErrorPart2: 'Voulez-vous continuer ?',
|
appCreateDSLErrorPart2: 'Voulez-vous continuer ?',
|
||||||
foundResults: '{{compte}} Résultats',
|
foundResults: '{{count}} Résultats',
|
||||||
workflowShortDescription: 'Flux agentique pour automatisations intelligentes',
|
workflowShortDescription: 'Flux agentique pour automatisations intelligentes',
|
||||||
agentShortDescription: 'Agent intelligent avec raisonnement et utilisation autonome de l’outil',
|
agentShortDescription: 'Agent intelligent avec raisonnement et utilisation autonome de l’outil',
|
||||||
learnMore: 'Pour en savoir plus',
|
learnMore: 'Pour en savoir plus',
|
||||||
@ -75,7 +75,7 @@ const translation = {
|
|||||||
completionUserDescription: 'Créez rapidement un assistant IA pour les tâches de génération de texte avec une configuration simple.',
|
completionUserDescription: 'Créez rapidement un assistant IA pour les tâches de génération de texte avec une configuration simple.',
|
||||||
agentUserDescription: 'Un agent intelligent capable d’un raisonnement itératif et d’une utilisation autonome d’outils pour atteindre les objectifs de la tâche.',
|
agentUserDescription: 'Un agent intelligent capable d’un raisonnement itératif et d’une utilisation autonome d’outils pour atteindre les objectifs de la tâche.',
|
||||||
forBeginners: 'Types d’applications plus basiques',
|
forBeginners: 'Types d’applications plus basiques',
|
||||||
foundResult: '{{compte}} Résultat',
|
foundResult: '{{count}} Résultat',
|
||||||
noIdeaTip: 'Pas d’idées ? Consultez nos modèles',
|
noIdeaTip: 'Pas d’idées ? Consultez nos modèles',
|
||||||
optional: 'Optionnel',
|
optional: 'Optionnel',
|
||||||
advancedShortDescription: 'Workflow amélioré pour conversations multi-tours',
|
advancedShortDescription: 'Workflow amélioré pour conversations multi-tours',
|
||||||
@ -258,7 +258,7 @@ const translation = {
|
|||||||
searchKnowledgeBasesDesc: 'Recherchez et accédez à vos bases de connaissances',
|
searchKnowledgeBasesDesc: 'Recherchez et accédez à vos bases de connaissances',
|
||||||
searchWorkflowNodesDesc: 'Recherchez et accédez aux nœuds du flux de travail actuel par nom ou type',
|
searchWorkflowNodesDesc: 'Recherchez et accédez aux nœuds du flux de travail actuel par nom ou type',
|
||||||
searchApplicationsDesc: 'Recherchez et accédez à vos applications',
|
searchApplicationsDesc: 'Recherchez et accédez à vos applications',
|
||||||
searchPlugins: 'Plugins de recherche',
|
searchPlugins: 'Rechercher des plugins',
|
||||||
searchWorkflowNodes: 'Rechercher des nœuds de workflow',
|
searchWorkflowNodes: 'Rechercher des nœuds de workflow',
|
||||||
searchKnowledgeBases: 'Rechercher dans les bases de connaissances',
|
searchKnowledgeBases: 'Rechercher dans les bases de connaissances',
|
||||||
searchApplications: 'Rechercher des applications',
|
searchApplications: 'Rechercher des applications',
|
||||||
|
|||||||
@ -162,7 +162,7 @@ const translation = {
|
|||||||
general: 'Généralités',
|
general: 'Généralités',
|
||||||
fullDocTip: 'L’intégralité du document est utilisée comme morceau parent et récupérée directement. Veuillez noter que pour des raisons de performance, le texte dépassant 10000 jetons sera automatiquement tronqué.',
|
fullDocTip: 'L’intégralité du document est utilisée comme morceau parent et récupérée directement. Veuillez noter que pour des raisons de performance, le texte dépassant 10000 jetons sera automatiquement tronqué.',
|
||||||
fullDoc: 'Doc complet',
|
fullDoc: 'Doc complet',
|
||||||
previewChunkCount: '{{compte}} Tronçons estimés',
|
previewChunkCount: '{{count}} Tronçons estimés',
|
||||||
childChunkForRetrieval: 'Child-chunk pour l’extraction',
|
childChunkForRetrieval: 'Child-chunk pour l’extraction',
|
||||||
parentChildDelimiterTip: 'Un délimiteur est le caractère utilisé pour séparer le texte. \\n\\n est recommandé pour diviser le document d’origine en gros morceaux parents. Vous pouvez également utiliser des délimiteurs spéciaux définis par vous-même.',
|
parentChildDelimiterTip: 'Un délimiteur est le caractère utilisé pour séparer le texte. \\n\\n est recommandé pour diviser le document d’origine en gros morceaux parents. Vous pouvez également utiliser des délimiteurs spéciaux définis par vous-même.',
|
||||||
qaSwitchHighQualityTipTitle: 'Le format Q&R nécessite une méthode d’indexation de haute qualité',
|
qaSwitchHighQualityTipTitle: 'Le format Q&R nécessite une méthode d’indexation de haute qualité',
|
||||||
|
|||||||
@ -749,8 +749,8 @@ const translation = {
|
|||||||
continueOnError: 'continuer sur l’erreur',
|
continueOnError: 'continuer sur l’erreur',
|
||||||
},
|
},
|
||||||
comma: ',',
|
comma: ',',
|
||||||
error_one: '{{compte}} Erreur',
|
error_one: '{{count}} Erreur',
|
||||||
error_other: '{{compte}} Erreurs',
|
error_other: '{{count}} Erreurs',
|
||||||
parallelModeEnableDesc: 'En mode parallèle, les tâches au sein des itérations prennent en charge l’exécution parallèle. Vous pouvez le configurer dans le panneau des propriétés à droite.',
|
parallelModeEnableDesc: 'En mode parallèle, les tâches au sein des itérations prennent en charge l’exécution parallèle. Vous pouvez le configurer dans le panneau des propriétés à droite.',
|
||||||
parallelModeUpper: 'MODE PARALLÈLE',
|
parallelModeUpper: 'MODE PARALLÈLE',
|
||||||
parallelPanelDesc: 'En mode parallèle, les tâches de l’itération prennent en charge l’exécution parallèle.',
|
parallelPanelDesc: 'En mode parallèle, les tâches de l’itération prennent en charge l’exécution parallèle.',
|
||||||
|
|||||||
@ -254,28 +254,28 @@ const translation = {
|
|||||||
maxActiveRequestsTip: 'प्रति ऐप सक्रिय अनुरोधों की अधिकतम संख्या (असीमित के लिए 0)',
|
maxActiveRequestsTip: 'प्रति ऐप सक्रिय अनुरोधों की अधिकतम संख्या (असीमित के लिए 0)',
|
||||||
gotoAnything: {
|
gotoAnything: {
|
||||||
actions: {
|
actions: {
|
||||||
searchPlugins: 'खोज प्लगइन्स',
|
searchPlugins: 'प्लगइन्स खोजें',
|
||||||
searchWorkflowNodes: 'खोज कार्यप्रवाह नोड्स',
|
searchWorkflowNodes: 'कार्यप्रवाह नोड्स खोजें',
|
||||||
searchKnowledgeBases: 'ज्ञान आधार खोजें',
|
searchKnowledgeBases: 'ज्ञान आधार खोजें',
|
||||||
searchApplications: 'अनुसंधान एप्लिकेशन',
|
searchApplications: 'एप्लिकेशन खोजें',
|
||||||
searchPluginsDesc: 'अपने प्लगइन्स को खोजें और नेविगेट करें',
|
searchPluginsDesc: 'अपने प्लगइन्स को खोजें और नेविगेट करें',
|
||||||
searchWorkflowNodesDesc: 'वर्तमान कार्यप्रवाह में नाम या प्रकार द्वारा नोड्स को खोजें और उन पर कूदें',
|
searchWorkflowNodesDesc: 'वर्तमान कार्यप्रवाह में नाम या प्रकार द्वारा नोड्स को खोजें और उन पर कूदें',
|
||||||
searchKnowledgeBasesDesc: 'अपने ज्ञान आधारों की खोज करें और उन्हें नेविगेट करें',
|
searchKnowledgeBasesDesc: 'अपने ज्ञान आधारों की खोज करें और उन्हें नेविगेट करें',
|
||||||
searchApplicationsDesc: 'अपने अनुप्रयोगों की खोज करें और उन्हें नेविगेट करें',
|
searchApplicationsDesc: 'अपने अनुप्रयोगों की खोज करें और उन्हें नेविगेट करें',
|
||||||
searchWorkflowNodesHelp: 'यह सुविधा केवल तब काम करती है जब आप एक कार्यप्रवाह देख रहे हों। पहले एक कार्यप्रवाह पर जाएं।',
|
searchWorkflowNodesHelp: 'यह सुविधा केवल तब काम करती है जब आप एक कार्यप्रवाह देख रहे हों। पहले एक कार्यप्रवाह पर जाएं।',
|
||||||
themeCategoryTitle: 'थीम',
|
themeCategoryTitle: 'थीम',
|
||||||
runTitle: 'आदेश',
|
runTitle: 'कमांड',
|
||||||
languageCategoryTitle: 'भाषा',
|
languageCategoryTitle: 'भाषा',
|
||||||
languageCategoryDesc: 'इंटरफेस भाषा बदलें',
|
languageCategoryDesc: 'इंटरफेस भाषा बदलें',
|
||||||
themeSystem: 'सिस्टम थीम',
|
themeSystem: 'सिस्टम थीम',
|
||||||
themeLight: 'लाइट थीम',
|
themeLight: 'लाइट थीम',
|
||||||
themeDarkDesc: 'अंधेरे रूप का उपयोग करें',
|
themeDarkDesc: 'डार्क उपस्थिति का प्रयोग करें',
|
||||||
themeDark: 'डार्क थीम',
|
themeDark: 'डार्क थीम',
|
||||||
themeLightDesc: 'हल्की उपस्थिति का प्रयोग करें',
|
themeLightDesc: 'हल्की उपस्थिति का प्रयोग करें',
|
||||||
languageChangeDesc: 'यूआई भाषा बदलें',
|
languageChangeDesc: 'इंटरफेस भाषा बदलें',
|
||||||
themeCategoryDesc: 'ऐप्लिकेशन थीम बदलें',
|
themeCategoryDesc: 'ऐप की थीम बदलें',
|
||||||
themeSystemDesc: 'अपने ऑपरेटिंग सिस्टम की उपस्थिति का पालन करें',
|
themeSystemDesc: 'अपने ऑपरेटिंग सिस्टम की उपस्थिति का पालन करें',
|
||||||
runDesc: 'त्वरित आदेश चलाएँ (थीम, भाषा, ...)',
|
runDesc: 'त्वरित कमांड चलाएँ (थीम, भाषा, ...)',
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
noPluginsFound: 'कोई प्लगइन नहीं मिले',
|
noPluginsFound: 'कोई प्लगइन नहीं मिले',
|
||||||
|
|||||||
@ -266,7 +266,7 @@ const translation = {
|
|||||||
searchApplications: 'Cerca applicazioni',
|
searchApplications: 'Cerca applicazioni',
|
||||||
searchPluginsDesc: 'Cerca e naviga verso i tuoi plugin',
|
searchPluginsDesc: 'Cerca e naviga verso i tuoi plugin',
|
||||||
searchKnowledgeBasesDesc: 'Cerca e naviga nelle tue knowledge base',
|
searchKnowledgeBasesDesc: 'Cerca e naviga nelle tue knowledge base',
|
||||||
searchPlugins: 'Plugin di ricerca',
|
searchPlugins: 'Cerca plugin',
|
||||||
searchWorkflowNodesDesc: 'Trovare e passare ai nodi nel flusso di lavoro corrente in base al nome o al tipo',
|
searchWorkflowNodesDesc: 'Trovare e passare ai nodi nel flusso di lavoro corrente in base al nome o al tipo',
|
||||||
searchKnowledgeBases: 'Cerca nelle Basi di Conoscenza',
|
searchKnowledgeBases: 'Cerca nelle Basi di Conoscenza',
|
||||||
themeCategoryTitle: 'Tema',
|
themeCategoryTitle: 'Tema',
|
||||||
|
|||||||
@ -258,11 +258,11 @@ const translation = {
|
|||||||
searchApplicationsDesc: 'Pesquise e navegue até seus aplicativos',
|
searchApplicationsDesc: 'Pesquise e navegue até seus aplicativos',
|
||||||
searchPluginsDesc: 'Pesquise e navegue até seus plug-ins',
|
searchPluginsDesc: 'Pesquise e navegue até seus plug-ins',
|
||||||
searchKnowledgeBases: 'Pesquisar bases de conhecimento',
|
searchKnowledgeBases: 'Pesquisar bases de conhecimento',
|
||||||
searchApplications: 'Aplicativos de pesquisa',
|
searchApplications: 'Pesquisar aplicativos',
|
||||||
searchWorkflowNodesDesc: 'Localizar e ir para nós no fluxo de trabalho atual por nome ou tipo',
|
searchWorkflowNodesDesc: 'Localizar e ir para nós no fluxo de trabalho atual por nome ou tipo',
|
||||||
searchWorkflowNodesHelp: 'Esse recurso só funciona ao visualizar um fluxo de trabalho. Navegue até um fluxo de trabalho primeiro.',
|
searchWorkflowNodesHelp: 'Esse recurso só funciona ao visualizar um fluxo de trabalho. Navegue até um fluxo de trabalho primeiro.',
|
||||||
searchKnowledgeBasesDesc: 'Pesquise e navegue até suas bases de conhecimento',
|
searchKnowledgeBasesDesc: 'Pesquise e navegue até suas bases de conhecimento',
|
||||||
searchWorkflowNodes: 'Nós de fluxo de trabalho de pesquisa',
|
searchWorkflowNodes: 'Pesquisar nós de fluxo de trabalho',
|
||||||
themeDarkDesc: 'Use aparência escura',
|
themeDarkDesc: 'Use aparência escura',
|
||||||
themeCategoryDesc: 'Mudar o tema do aplicativo',
|
themeCategoryDesc: 'Mudar o tema do aplicativo',
|
||||||
themeLight: 'Tema Claro',
|
themeLight: 'Tema Claro',
|
||||||
|
|||||||
@ -254,7 +254,7 @@ const translation = {
|
|||||||
maxActiveRequestsTip: 'Максимальное количество одновременно активных запросов на одно приложение (0 для неограниченного количества)',
|
maxActiveRequestsTip: 'Максимальное количество одновременно активных запросов на одно приложение (0 для неограниченного количества)',
|
||||||
gotoAnything: {
|
gotoAnything: {
|
||||||
actions: {
|
actions: {
|
||||||
searchPlugins: 'Поисковые плагины',
|
searchPlugins: 'Поиск плагинов',
|
||||||
searchKnowledgeBases: 'Поиск в базах знаний',
|
searchKnowledgeBases: 'Поиск в базах знаний',
|
||||||
searchApplications: 'Поиск приложений',
|
searchApplications: 'Поиск приложений',
|
||||||
searchKnowledgeBasesDesc: 'Поиск и переход к базам знаний',
|
searchKnowledgeBasesDesc: 'Поиск и переход к базам знаний',
|
||||||
@ -269,11 +269,11 @@ const translation = {
|
|||||||
themeCategoryTitle: 'Тема',
|
themeCategoryTitle: 'Тема',
|
||||||
languageCategoryTitle: 'Язык',
|
languageCategoryTitle: 'Язык',
|
||||||
themeSystem: 'Системная тема',
|
themeSystem: 'Системная тема',
|
||||||
runDesc: 'Запустите быстрые команды (тема, язык, ...)',
|
runDesc: 'Запустите быстрые команды (тема, язык, …)',
|
||||||
themeLight: 'Светлая тема',
|
themeLight: 'Светлая тема',
|
||||||
themeDarkDesc: 'Используйте темный внешний вид',
|
themeDarkDesc: 'Используйте темный внешний вид',
|
||||||
languageChangeDesc: 'Изменить язык интерфейса',
|
languageChangeDesc: 'Измените язык интерфейса',
|
||||||
languageCategoryDesc: 'Переключить язык интерфейса',
|
languageCategoryDesc: 'Переключите язык интерфейса',
|
||||||
themeLightDesc: 'Используйте светлый внешний вид',
|
themeLightDesc: 'Используйте светлый внешний вид',
|
||||||
themeSystemDesc: 'Следуйте внешнему виду вашей операционной системы',
|
themeSystemDesc: 'Следуйте внешнему виду вашей операционной системы',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -258,7 +258,7 @@ const translation = {
|
|||||||
searchKnowledgeBasesDesc: 'Iskanje in krmarjenje do zbirk znanja',
|
searchKnowledgeBasesDesc: 'Iskanje in krmarjenje do zbirk znanja',
|
||||||
searchWorkflowNodesHelp: 'Ta funkcija deluje le pri ogledu poteka dela. Najprej se pomaknite do poteka dela.',
|
searchWorkflowNodesHelp: 'Ta funkcija deluje le pri ogledu poteka dela. Najprej se pomaknite do poteka dela.',
|
||||||
searchApplicationsDesc: 'Iskanje in krmarjenje do aplikacij',
|
searchApplicationsDesc: 'Iskanje in krmarjenje do aplikacij',
|
||||||
searchPlugins: 'Iskalni vtičniki',
|
searchPlugins: 'Iskanje vtičnikov',
|
||||||
searchApplications: 'Iskanje aplikacij',
|
searchApplications: 'Iskanje aplikacij',
|
||||||
searchWorkflowNodesDesc: 'Iskanje vozlišč in skok nanje v trenutnem poteku dela po imenu ali vrsti',
|
searchWorkflowNodesDesc: 'Iskanje vozlišč in skok nanje v trenutnem poteku dela po imenu ali vrsti',
|
||||||
searchKnowledgeBases: 'Iskanje po zbirkah znanja',
|
searchKnowledgeBases: 'Iskanje po zbirkah znanja',
|
||||||
|
|||||||
@ -37,11 +37,11 @@ const translation = {
|
|||||||
captionName: 'ไอคอนและชื่อโปรเจกต์',
|
captionName: 'ไอคอนและชื่อโปรเจกต์',
|
||||||
appNamePlaceholder: 'ตั้งชื่อโปรเจกต์ของคุณ',
|
appNamePlaceholder: 'ตั้งชื่อโปรเจกต์ของคุณ',
|
||||||
captionDescription: 'คำอธิบาย',
|
captionDescription: 'คำอธิบาย',
|
||||||
appDescriptionPlaceholder: 'ป้อนคําอธิบายของโปรเจกต์',
|
appDescriptionPlaceholder: 'ป้อนคำอธิบายของโปรเจกต์',
|
||||||
useTemplate: 'ใช้เทมเพลตนี้',
|
useTemplate: 'ใช้เทมเพลตนี้',
|
||||||
previewDemo: 'ตัวอย่างการใช้งาน',
|
previewDemo: 'ตัวอย่างการใช้งาน',
|
||||||
chatApp: 'ผู้ช่วย',
|
chatApp: 'ผู้ช่วย',
|
||||||
chatAppIntro: 'ฉันต้องการสร้างโปรเจกต์ ที่เป็นแอปพลิเคชันที่ใช้การแชท โปรเจกต์นี้ใช้รูปแบบคําถามและคําตอบ ทําให้สามารถสนทนาต่อเนื่องได้หลายรอบ(Multi-turn)',
|
chatAppIntro: 'ฉันต้องการสร้างโปรเจกต์ ที่เป็นแอปพลิเคชันที่ใช้การแชท โปรเจกต์นี้ใช้รูปแบบคำถามและคำตอบ ทําให้สามารถสนทนาต่อเนื่องได้หลายรอบ(Multi-turn)',
|
||||||
agentAssistant: 'ผู้ช่วยใหม่',
|
agentAssistant: 'ผู้ช่วยใหม่',
|
||||||
completeApp: 'เครื่องมือสร้างข้อความ',
|
completeApp: 'เครื่องมือสร้างข้อความ',
|
||||||
completeAppIntro: 'ฉันต้องการสร้างโปรเจกต์ที่ ที่สามารถสร้างข้อความคุณภาพสูงตามข้อความแจ้ง เช่น การสร้างบทความ สรุป การแปล และอื่นๆ',
|
completeAppIntro: 'ฉันต้องการสร้างโปรเจกต์ที่ ที่สามารถสร้างข้อความคุณภาพสูงตามข้อความแจ้ง เช่น การสร้างบทความ สรุป การแปล และอื่นๆ',
|
||||||
@ -294,7 +294,7 @@ const translation = {
|
|||||||
searchTemporarilyUnavailable: 'การค้นหาไม่พร้อมใช้งานชั่วคราว',
|
searchTemporarilyUnavailable: 'การค้นหาไม่พร้อมใช้งานชั่วคราว',
|
||||||
someServicesUnavailable: 'บริการค้นหาบางบริการไม่พร้อมใช้งาน',
|
someServicesUnavailable: 'บริการค้นหาบางบริการไม่พร้อมใช้งาน',
|
||||||
clearToSearchAll: 'ล้าง @ เพื่อค้นหาทั้งหมด',
|
clearToSearchAll: 'ล้าง @ เพื่อค้นหาทั้งหมด',
|
||||||
searchPlaceholder: 'ค้นหาหรือพิมพ์ @ สําหรับคําสั่ง...',
|
searchPlaceholder: 'ค้นหาหรือพิมพ์ @ สำหรับคำสั่ง...',
|
||||||
servicesUnavailableMessage: 'บริการค้นหาบางบริการอาจประสบปัญหา ลองอีกครั้งในอีกสักครู่',
|
servicesUnavailableMessage: 'บริการค้นหาบางบริการอาจประสบปัญหา ลองอีกครั้งในอีกสักครู่',
|
||||||
searching: 'กำลังค้นหา...',
|
searching: 'กำลังค้นหา...',
|
||||||
searchHint: 'เริ่มพิมพ์เพื่อค้นหาทุกอย่างได้ทันที',
|
searchHint: 'เริ่มพิมพ์เพื่อค้นหาทุกอย่างได้ทันที',
|
||||||
@ -303,7 +303,7 @@ const translation = {
|
|||||||
resultCount: '{{count}} ผลลัพธ์',
|
resultCount: '{{count}} ผลลัพธ์',
|
||||||
resultCount_other: '{{count}} ผลลัพธ์',
|
resultCount_other: '{{count}} ผลลัพธ์',
|
||||||
inScope: 'ใน {{scope}}s',
|
inScope: 'ใน {{scope}}s',
|
||||||
noMatchingCommands: 'ไม่พบคําสั่งที่ตรงกัน',
|
noMatchingCommands: 'ไม่พบคำสั่งที่ตรงกัน',
|
||||||
tryDifferentSearch: 'ลองใช้ข้อความค้นหาอื่น',
|
tryDifferentSearch: 'ลองใช้ข้อความค้นหาอื่น',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -252,11 +252,10 @@ const translation = {
|
|||||||
actions: {
|
actions: {
|
||||||
searchKnowledgeBasesDesc: 'Bilgi bankalarınızda arama yapın ve bu forumlara gidin',
|
searchKnowledgeBasesDesc: 'Bilgi bankalarınızda arama yapın ve bu forumlara gidin',
|
||||||
searchWorkflowNodesDesc: 'Geçerli iş akışındaki düğümleri ada veya türe göre bulun ve atlayın',
|
searchWorkflowNodesDesc: 'Geçerli iş akışındaki düğümleri ada veya türe göre bulun ve atlayın',
|
||||||
searchApplications: 'Arama Uygulamaları',
|
searchApplications: 'Uygulamaları Ara',
|
||||||
searchKnowledgeBases: 'Bilgi Bankalarında Ara',
|
searchKnowledgeBases: 'Bilgi Bankalarında Ara',
|
||||||
searchWorkflowNodes: 'Arama İş Akışı Düğümleri',
|
searchWorkflowNodes: 'İş Akışı Düğümlerini Ara',
|
||||||
searchPluginsDesc: 'Eklentilerinizi arayın ve eklentilerinize gidin',
|
searchPlugins: 'Eklentileri Ara',
|
||||||
searchPlugins: 'Arama Eklentileri',
|
|
||||||
searchWorkflowNodesHelp: 'Bu özellik yalnızca bir iş akışını görüntülerken çalışır. Önce bir iş akışına gidin.',
|
searchWorkflowNodesHelp: 'Bu özellik yalnızca bir iş akışını görüntülerken çalışır. Önce bir iş akışına gidin.',
|
||||||
searchApplicationsDesc: 'Uygulamalarınızı arayın ve uygulamalarınıza gidin',
|
searchApplicationsDesc: 'Uygulamalarınızı arayın ve uygulamalarınıza gidin',
|
||||||
languageChangeDesc: 'UI dilini değiştir',
|
languageChangeDesc: 'UI dilini değiştir',
|
||||||
|
|||||||
@ -256,22 +256,22 @@ const translation = {
|
|||||||
actions: {
|
actions: {
|
||||||
searchApplications: 'Пошук додатків',
|
searchApplications: 'Пошук додатків',
|
||||||
searchKnowledgeBases: 'Пошук по базах знань',
|
searchKnowledgeBases: 'Пошук по базах знань',
|
||||||
searchWorkflowNodes: 'Вузли документообігу пошуку',
|
searchWorkflowNodes: 'Пошук вузлів робочого процесу',
|
||||||
searchApplicationsDesc: 'Шукайте та переходьте до своїх програм',
|
searchApplicationsDesc: 'Шукайте та переходьте до своїх програм',
|
||||||
searchPluginsDesc: 'Пошук і навігація до ваших плагінів',
|
searchPluginsDesc: 'Пошук і навігація до ваших плагінів',
|
||||||
searchWorkflowNodesHelp: 'Ця функція працює лише під час перегляду робочого процесу. Спочатку перейдіть до робочого процесу.',
|
searchWorkflowNodesHelp: 'Ця функція працює лише під час перегляду робочого процесу. Спочатку перейдіть до робочого процесу.',
|
||||||
searchPlugins: 'Пошукові плагіни',
|
searchPlugins: 'Пошук плагінів',
|
||||||
searchKnowledgeBasesDesc: 'Шукайте та переходьте до своїх баз знань',
|
searchKnowledgeBasesDesc: 'Шукайте та переходьте до своїх баз знань',
|
||||||
searchWorkflowNodesDesc: 'Знаходьте вузли в поточному робочому процесі та переходьте до них за іменем або типом',
|
searchWorkflowNodesDesc: 'Знаходьте вузли в поточному робочому процесі та переходьте до них за іменем або типом',
|
||||||
themeSystem: 'Тема системи',
|
themeSystem: 'Системна тема',
|
||||||
languageCategoryTitle: 'Мова',
|
languageCategoryTitle: 'Мова',
|
||||||
themeCategoryTitle: 'Тема',
|
themeCategoryTitle: 'Тема',
|
||||||
themeLight: 'Світла тема',
|
themeLight: 'Світла тема',
|
||||||
runTitle: 'Команди',
|
runTitle: 'Команди',
|
||||||
languageChangeDesc: 'Змінити мову інтерфейсу',
|
languageChangeDesc: 'Змінити мову інтерфейсу',
|
||||||
themeDark: 'Темний режим',
|
themeDark: 'Темна тема',
|
||||||
themeDarkDesc: 'Використовуйте темний режим',
|
themeDarkDesc: 'Використовуйте темний режим',
|
||||||
runDesc: 'Run quick commands (theme, language, ...)',
|
runDesc: 'Запустіть швидкі команди (тема, мова, ...)',
|
||||||
themeCategoryDesc: 'Переключити тему застосунку',
|
themeCategoryDesc: 'Переключити тему застосунку',
|
||||||
themeLightDesc: 'Використовуйте світлий вигляд',
|
themeLightDesc: 'Використовуйте світлий вигляд',
|
||||||
themeSystemDesc: 'Дотримуйтесь зовнішнього вигляду вашої операційної системи',
|
themeSystemDesc: 'Дотримуйтесь зовнішнього вигляду вашої операційної системи',
|
||||||
|
|||||||
@ -16,7 +16,10 @@ const translation = {
|
|||||||
viewBilling: '管理账单及订阅',
|
viewBilling: '管理账单及订阅',
|
||||||
buyPermissionDeniedTip: '请联系企业管理员订阅',
|
buyPermissionDeniedTip: '请联系企业管理员订阅',
|
||||||
plansCommon: {
|
plansCommon: {
|
||||||
title: '为您的 AI 之旅提供动力的定价套餐',
|
title: {
|
||||||
|
plans: '方案',
|
||||||
|
description: '选择最适合您团队需求的方案。',
|
||||||
|
},
|
||||||
freeTrialTipPrefix: '注册即可',
|
freeTrialTipPrefix: '注册即可',
|
||||||
freeTrialTip: '免费试用 200 个 OpenAI 消息额度',
|
freeTrialTip: '免费试用 200 个 OpenAI 消息额度',
|
||||||
freeTrialTipSuffix: '。无需信用卡',
|
freeTrialTipSuffix: '。无需信用卡',
|
||||||
@ -32,7 +35,7 @@ const translation = {
|
|||||||
year: '年',
|
year: '年',
|
||||||
save: '节省',
|
save: '节省',
|
||||||
free: '免费',
|
free: '免费',
|
||||||
annualBilling: '按年计费',
|
annualBilling: '按年计费节省 {{percent}}%',
|
||||||
comparePlanAndFeatures: '对比套餐 & 功能特性',
|
comparePlanAndFeatures: '对比套餐 & 功能特性',
|
||||||
priceTip: '每个团队空间/',
|
priceTip: '每个团队空间/',
|
||||||
currentPlan: '当前计划',
|
currentPlan: '当前计划',
|
||||||
|
|||||||
@ -162,7 +162,7 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
|
|||||||
...baseHooks.beforeRequest || [],
|
...baseHooks.beforeRequest || [],
|
||||||
isPublicAPI && beforeRequestPublicAuthorization,
|
isPublicAPI && beforeRequestPublicAuthorization,
|
||||||
!isPublicAPI && !isMarketplaceAPI && beforeRequestAuthorization,
|
!isPublicAPI && !isMarketplaceAPI && beforeRequestAuthorization,
|
||||||
].filter(Boolean),
|
].filter((h): h is BeforeRequestHook => Boolean(h)),
|
||||||
afterResponse: [
|
afterResponse: [
|
||||||
...baseHooks.afterResponse || [],
|
...baseHooks.afterResponse || [],
|
||||||
afterResponseErrorCode(otherOptions),
|
afterResponseErrorCode(otherOptions),
|
||||||
|
|||||||
@ -87,6 +87,9 @@ const config = {
|
|||||||
2: '0.02',
|
2: '0.02',
|
||||||
8: '0.08',
|
8: '0.08',
|
||||||
},
|
},
|
||||||
|
fontFamily: {
|
||||||
|
instrument: ['var(--font-instrument-serif)', 'serif'],
|
||||||
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
'2xs': '0.625rem',
|
'2xs': '0.625rem',
|
||||||
},
|
},
|
||||||
@ -129,6 +132,7 @@ const config = {
|
|||||||
'tag-selector-mask-hover-bg': 'var(--color-tag-selector-mask-hover-bg)',
|
'tag-selector-mask-hover-bg': 'var(--color-tag-selector-mask-hover-bg)',
|
||||||
'pipeline-template-card-hover-bg': 'var(--color-pipeline-template-card-hover-bg)',
|
'pipeline-template-card-hover-bg': 'var(--color-pipeline-template-card-hover-bg)',
|
||||||
'pipeline-add-documents-title-bg': 'var(--color-pipeline-add-documents-title-bg)',
|
'pipeline-add-documents-title-bg': 'var(--color-pipeline-add-documents-title-bg)',
|
||||||
|
'billing-plan-title-bg': 'var(--color-billing-plan-title-bg)',
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'spin-slow': 'spin 2s linear infinite',
|
'spin-slow': 'spin 2s linear infinite',
|
||||||
|
|||||||
@ -69,4 +69,5 @@ html[data-theme="dark"] {
|
|||||||
--color-pipeline-template-card-hover-bg: linear-gradient(0deg, rgba(58, 58, 64, 1) 60.27%, rgba(58, 58, 64, 0) 100%);
|
--color-pipeline-template-card-hover-bg: linear-gradient(0deg, rgba(58, 58, 64, 1) 60.27%, rgba(58, 58, 64, 0) 100%);
|
||||||
--color-pipeline-add-documents-title-bg: linear-gradient(92deg, rgba(54, 191, 250, 1) 0%, rgba(41, 109, 255, 1) 97.78%);
|
--color-pipeline-add-documents-title-bg: linear-gradient(92deg, rgba(54, 191, 250, 1) 0%, rgba(41, 109, 255, 1) 97.78%);
|
||||||
--color-background-gradient-bg-fill-chat-bubble-bg-3: #27314d;
|
--color-background-gradient-bg-fill-chat-bubble-bg-3: #27314d;
|
||||||
|
--color-billing-plan-title-bg: linear-gradient(95deg, #0A68FF 29.47%, #03F 105.31%);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,4 +69,5 @@ html[data-theme="light"] {
|
|||||||
--color-pipeline-template-card-hover-bg: linear-gradient(0deg, rgba(249, 250, 251, 1) 60.27%, rgba(249, 250, 251, 0) 100%);
|
--color-pipeline-template-card-hover-bg: linear-gradient(0deg, rgba(249, 250, 251, 1) 60.27%, rgba(249, 250, 251, 0) 100%);
|
||||||
--color-pipeline-add-documents-title-bg: linear-gradient(92deg, rgba(11, 165, 236, 0.95) 0%, rgba(21, 90, 239, 0.95) 97.78%);
|
--color-pipeline-add-documents-title-bg: linear-gradient(92deg, rgba(11, 165, 236, 0.95) 0%, rgba(21, 90, 239, 0.95) 97.78%);
|
||||||
--color-background-gradient-bg-fill-chat-bubble-bg-3: #e1effe;
|
--color-background-gradient-bg-fill-chat-bubble-bg-3: #e1effe;
|
||||||
|
--color-billing-plan-title-bg: linear-gradient(95deg, #03F 29.47%, #03F 105.31%);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ export const mergeValidCompletionParams = (
|
|||||||
if (!oldParams || Object.keys(oldParams).length === 0)
|
if (!oldParams || Object.keys(oldParams).length === 0)
|
||||||
return { params: {}, removedDetails: {} }
|
return { params: {}, removedDetails: {} }
|
||||||
|
|
||||||
const acceptedKeys = new Set(rules.map(r => r.name))
|
|
||||||
const ruleMap: Record<string, ModelParameterRule> = {}
|
const ruleMap: Record<string, ModelParameterRule> = {}
|
||||||
rules.forEach((r) => {
|
rules.forEach((r) => {
|
||||||
ruleMap[r.name] = r
|
ruleMap[r.name] = r
|
||||||
@ -17,11 +16,6 @@ export const mergeValidCompletionParams = (
|
|||||||
const removedDetails: Record<string, string> = {}
|
const removedDetails: Record<string, string> = {}
|
||||||
|
|
||||||
Object.entries(oldParams).forEach(([key, value]) => {
|
Object.entries(oldParams).forEach(([key, value]) => {
|
||||||
if (!acceptedKeys.has(key)) {
|
|
||||||
removedDetails[key] = 'unsupported'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const rule = ruleMap[key]
|
const rule = ruleMap[key]
|
||||||
if (!rule) {
|
if (!rule) {
|
||||||
removedDetails[key] = 'unsupported'
|
removedDetails[key] = 'unsupported'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user