mirror of
https://github.com/langgenius/dify.git
synced 2026-02-05 19:25:32 +08:00
Compare commits
24 Commits
chore/auto
...
1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 46036e6ce6 | |||
| 1ffda0dd34 | |||
| da01b460fe | |||
| 90a1508b87 | |||
| b07016113c | |||
| d8317fcf81 | |||
| a6bc642721 | |||
| b730f243dc | |||
| 71a57275ab | |||
| 41bf8d925f | |||
| 6d172498d1 | |||
| cad58658c2 | |||
| a58b990855 | |||
| b6b1903a37 | |||
| ed5596a8f4 | |||
| 49d0acd188 | |||
| 58a74fe1fb | |||
| a1ab4aec3d | |||
| f77f7e1437 | |||
| adda049265 | |||
| 9b2a9260ef | |||
| c8cc31af88 | |||
| d333de274f | |||
| 9e220d5d30 |
13
.github/ISSUE_TEMPLATE/tracker.yml
vendored
Normal file
13
.github/ISSUE_TEMPLATE/tracker.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
name: "👾 Tracker"
|
||||
description: For inner usages, please donot use this template.
|
||||
title: "[Tracker] "
|
||||
labels:
|
||||
- tracker
|
||||
body:
|
||||
- type: textarea
|
||||
id: content
|
||||
attributes:
|
||||
label: Blockers
|
||||
placeholder: "- [ ] ..."
|
||||
validations:
|
||||
required: true
|
||||
@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
||||
|
||||
CURRENT_VERSION: str = Field(
|
||||
description="Dify version",
|
||||
default="1.0.0",
|
||||
default="1.0.1",
|
||||
)
|
||||
|
||||
COMMIT_SHA: str = Field(
|
||||
|
||||
@ -316,7 +316,7 @@ class AppTraceApi(Resource):
|
||||
@account_initialization_required
|
||||
def post(self, app_id):
|
||||
# add app trace
|
||||
if not current_user.is_admin_or_owner:
|
||||
if not current_user.is_editing_role:
|
||||
raise Forbidden()
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("enabled", type=bool, required=True, location="json")
|
||||
|
||||
@ -122,7 +122,7 @@ class DataSourceNotionListApi(Resource):
|
||||
if dataset.data_source_type != "notion_import":
|
||||
raise ValueError("Dataset is not notion type.")
|
||||
|
||||
documents = session.execute(
|
||||
documents = session.scalars(
|
||||
select(Document).filter_by(
|
||||
dataset_id=dataset_id,
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
|
||||
@ -10,7 +10,12 @@ from controllers.console import api
|
||||
from controllers.console.apikey import api_key_fields, api_key_list
|
||||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
||||
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
enterprise_license_required,
|
||||
setup_required,
|
||||
)
|
||||
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||
from core.indexing_runner import IndexingRunner
|
||||
from core.model_runtime.entities.model_entities import ModelType
|
||||
@ -96,6 +101,7 @@ class DatasetListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument(
|
||||
@ -210,6 +216,7 @@ class DatasetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
@ -276,7 +283,11 @@ class DatasetApi(Resource):
|
||||
data = request.get_json()
|
||||
|
||||
# check embedding model setting
|
||||
if data.get("indexing_technique") == "high_quality":
|
||||
if (
|
||||
data.get("indexing_technique") == "high_quality"
|
||||
and data.get("embedding_model_provider") is not None
|
||||
and data.get("embedding_model") is not None
|
||||
):
|
||||
DatasetService.check_embedding_model_setting(
|
||||
dataset.tenant_id, data.get("embedding_model_provider"), data.get("embedding_model")
|
||||
)
|
||||
@ -313,6 +324,7 @@ class DatasetApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ from controllers.console.datasets.error import (
|
||||
)
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
@ -242,6 +243,7 @@ class DatasetDocumentListApi(Resource):
|
||||
@account_initialization_required
|
||||
@marshal_with(documents_and_batch_fields)
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id):
|
||||
dataset_id = str(dataset_id)
|
||||
|
||||
@ -297,6 +299,7 @@ class DatasetDocumentListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id):
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
@ -320,6 +323,7 @@ class DatasetInitApi(Resource):
|
||||
@account_initialization_required
|
||||
@marshal_with(dataset_and_document_fields)
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self):
|
||||
# The role of the current user in the ta table must be admin, owner, or editor
|
||||
if not current_user.is_editor:
|
||||
@ -694,6 +698,7 @@ class DocumentProcessingApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
document_id = str(document_id)
|
||||
@ -730,6 +735,7 @@ class DocumentDeleteApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id, document_id):
|
||||
dataset_id = str(dataset_id)
|
||||
document_id = str(document_id)
|
||||
@ -798,6 +804,7 @@ class DocumentStatusApi(DocumentResource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
@ -893,6 +900,7 @@ class DocumentPauseApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id):
|
||||
"""pause document."""
|
||||
dataset_id = str(dataset_id)
|
||||
@ -925,6 +933,7 @@ class DocumentRecoverApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id):
|
||||
"""recover document."""
|
||||
dataset_id = str(dataset_id)
|
||||
@ -954,6 +963,7 @@ class DocumentRetryApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id):
|
||||
"""retry document."""
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ from controllers.console.datasets.error import (
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_knowledge_limit_check,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
cloud_edition_billing_resource_check,
|
||||
setup_required,
|
||||
)
|
||||
@ -106,6 +107,7 @@ class DatasetDocumentSegmentListApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@ -137,6 +139,7 @@ class DatasetDocumentSegmentApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
@ -191,6 +194,7 @@ class DatasetDocumentSegmentAddApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_knowledge_limit_check("add_segment")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@ -240,6 +244,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@ -299,6 +304,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@ -336,6 +342,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_knowledge_limit_check("add_segment")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@ -402,6 +409,7 @@ class ChildChunkAddApi(Resource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_knowledge_limit_check("add_segment")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@ -499,6 +507,7 @@ class ChildChunkAddApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@ -542,6 +551,7 @@ class ChildChunkUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def delete(self, dataset_id, document_id, segment_id, child_chunk_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@ -586,6 +596,7 @@ class ChildChunkUpdateApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, segment_id, child_chunk_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
|
||||
@ -2,7 +2,11 @@ from flask_restful import Resource # type: ignore
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_rate_limit_check,
|
||||
setup_required,
|
||||
)
|
||||
from libs.login import login_required
|
||||
|
||||
|
||||
@ -10,6 +14,7 @@ class HitTestingApi(Resource, DatasetsHitTestingBase):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def post(self, dataset_id):
|
||||
dataset_id_str = str(dataset_id)
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ from libs.helper import TimestampField
|
||||
from libs.login import login_required
|
||||
from models.account import Tenant, TenantStatus
|
||||
from services.account_service import TenantService
|
||||
from services.feature_service import FeatureService
|
||||
from services.file_service import FileService
|
||||
from services.workspace_service import WorkspaceService
|
||||
|
||||
@ -68,6 +69,11 @@ class TenantListApi(Resource):
|
||||
tenants = TenantService.get_join_tenants(current_user)
|
||||
|
||||
for tenant in tenants:
|
||||
features = FeatureService.get_features(tenant.id)
|
||||
if features.billing.enabled:
|
||||
tenant.plan = features.billing.subscription.plan
|
||||
else:
|
||||
tenant.plan = "sandbox"
|
||||
if tenant.id == current_user.current_tenant_id:
|
||||
tenant.current = True # Set current=True for current tenant
|
||||
return {"workspaces": marshal(tenants, tenants_fields)}, 200
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from flask import abort, request
|
||||
@ -8,6 +9,8 @@ from flask_login import current_user # type: ignore
|
||||
from configs import dify_config
|
||||
from controllers.console.workspace.error import AccountNotInitializedError
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.dataset import RateLimitLog
|
||||
from models.model import DifySetup
|
||||
from services.feature_service import FeatureService, LicenseStatus
|
||||
from services.operation_service import OperationService
|
||||
@ -67,7 +70,9 @@ def cloud_edition_billing_resource_check(resource: str):
|
||||
elif resource == "apps" and 0 < apps.limit <= apps.size:
|
||||
abort(403, "The number of apps has reached the limit of your subscription.")
|
||||
elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
|
||||
abort(403, "The capacity of the vector space has reached the limit of your subscription.")
|
||||
abort(
|
||||
403, "The capacity of the knowledge storage space has reached the limit of your subscription."
|
||||
)
|
||||
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
|
||||
# The api of file upload is used in the multiple places,
|
||||
# so we need to check the source of the request from datasets
|
||||
@ -112,6 +117,41 @@ def cloud_edition_billing_knowledge_limit_check(resource: str):
|
||||
return interceptor
|
||||
|
||||
|
||||
def cloud_edition_billing_rate_limit_check(resource: str):
|
||||
def interceptor(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
if resource == "knowledge":
|
||||
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(current_user.current_tenant_id)
|
||||
if knowledge_rate_limit.enabled:
|
||||
current_time = int(time.time() * 1000)
|
||||
key = f"rate_limit_{current_user.current_tenant_id}"
|
||||
|
||||
redis_client.zadd(key, {current_time: current_time})
|
||||
|
||||
redis_client.zremrangebyscore(key, 0, current_time - 60000)
|
||||
|
||||
request_count = redis_client.zcard(key)
|
||||
|
||||
if request_count > knowledge_rate_limit.limit:
|
||||
# add ratelimit record
|
||||
rate_limit_log = RateLimitLog(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
subscription_plan=knowledge_rate_limit.subscription_plan,
|
||||
operation="knowledge",
|
||||
)
|
||||
db.session.add(rate_limit_log)
|
||||
db.session.commit()
|
||||
abort(
|
||||
403, "Sorry, you have reached the knowledge base request rate limit of your subscription."
|
||||
)
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return interceptor
|
||||
|
||||
|
||||
def cloud_utm_record(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from enum import Enum
|
||||
@ -13,8 +14,10 @@ from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden, Unauthorized
|
||||
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs.login import _get_user
|
||||
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
|
||||
from models.dataset import RateLimitLog
|
||||
from models.model import ApiToken, App, EndUser
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
@ -139,6 +142,43 @@ def cloud_edition_billing_knowledge_limit_check(resource: str, api_token_type: s
|
||||
return interceptor
|
||||
|
||||
|
||||
def cloud_edition_billing_rate_limit_check(resource: str, api_token_type: str):
|
||||
def interceptor(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
api_token = validate_and_get_api_token(api_token_type)
|
||||
|
||||
if resource == "knowledge":
|
||||
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(api_token.tenant_id)
|
||||
if knowledge_rate_limit.enabled:
|
||||
current_time = int(time.time() * 1000)
|
||||
key = f"rate_limit_{api_token.tenant_id}"
|
||||
|
||||
redis_client.zadd(key, {current_time: current_time})
|
||||
|
||||
redis_client.zremrangebyscore(key, 0, current_time - 60000)
|
||||
|
||||
request_count = redis_client.zcard(key)
|
||||
|
||||
if request_count > knowledge_rate_limit.limit:
|
||||
# add ratelimit record
|
||||
rate_limit_log = RateLimitLog(
|
||||
tenant_id=api_token.tenant_id,
|
||||
subscription_plan=knowledge_rate_limit.subscription_plan,
|
||||
operation="knowledge",
|
||||
)
|
||||
db.session.add(rate_limit_log)
|
||||
db.session.commit()
|
||||
raise Forbidden(
|
||||
"Sorry, you have reached the knowledge base request rate limit of your subscription."
|
||||
)
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
return interceptor
|
||||
|
||||
|
||||
def validate_dataset_token(view=None):
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
|
||||
@ -7,7 +7,6 @@ from json import JSONDecodeError
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import or_
|
||||
|
||||
from constants import HIDDEN_VALUE
|
||||
from core.entities.model_entities import ModelStatus, ModelWithProviderEntity, SimpleModelProviderEntity
|
||||
@ -180,37 +179,35 @@ class ProviderConfiguration(BaseModel):
|
||||
else [],
|
||||
)
|
||||
|
||||
def _get_custom_provider_credentials(self) -> Provider | None:
|
||||
"""
|
||||
Get custom provider credentials.
|
||||
"""
|
||||
# get provider
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
provider_record = (
|
||||
db.session.query(Provider)
|
||||
.filter(
|
||||
Provider.tenant_id == self.tenant_id,
|
||||
Provider.provider_type == ProviderType.CUSTOM.value,
|
||||
Provider.provider_name.in_(provider_names),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
return provider_record
|
||||
|
||||
def custom_credentials_validate(self, credentials: dict) -> tuple[Provider | None, dict]:
|
||||
"""
|
||||
Validate custom credentials.
|
||||
:param credentials: provider credentials
|
||||
:return:
|
||||
"""
|
||||
# get provider
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_record = (
|
||||
db.session.query(Provider)
|
||||
.filter(
|
||||
Provider.tenant_id == self.tenant_id,
|
||||
Provider.provider_type == ProviderType.CUSTOM.value,
|
||||
or_(
|
||||
Provider.provider_name == model_provider_id.provider_name,
|
||||
Provider.provider_name == self.provider.provider,
|
||||
),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
else:
|
||||
provider_record = (
|
||||
db.session.query(Provider)
|
||||
.filter(
|
||||
Provider.tenant_id == self.tenant_id,
|
||||
Provider.provider_type == ProviderType.CUSTOM.value,
|
||||
Provider.provider_name == self.provider.provider,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
provider_record = self._get_custom_provider_credentials()
|
||||
|
||||
# Get provider credential secret variables
|
||||
provider_credential_secret_variables = self.extract_secret_variables(
|
||||
@ -291,18 +288,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:return:
|
||||
"""
|
||||
# get provider
|
||||
provider_record = (
|
||||
db.session.query(Provider)
|
||||
.filter(
|
||||
Provider.tenant_id == self.tenant_id,
|
||||
or_(
|
||||
Provider.provider_name == ModelProviderID(self.provider.provider).plugin_name,
|
||||
Provider.provider_name == self.provider.provider,
|
||||
),
|
||||
Provider.provider_type == ProviderType.CUSTOM.value,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
provider_record = self._get_custom_provider_credentials()
|
||||
|
||||
# delete provider
|
||||
if provider_record:
|
||||
@ -349,6 +335,33 @@ class ProviderConfiguration(BaseModel):
|
||||
|
||||
return None
|
||||
|
||||
def _get_custom_model_credentials(
|
||||
self,
|
||||
model_type: ModelType,
|
||||
model: str,
|
||||
) -> ProviderModel | None:
|
||||
"""
|
||||
Get custom model credentials.
|
||||
"""
|
||||
# get provider model
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
provider_model_record = (
|
||||
db.session.query(ProviderModel)
|
||||
.filter(
|
||||
ProviderModel.tenant_id == self.tenant_id,
|
||||
ProviderModel.provider_name.in_(provider_names),
|
||||
ProviderModel.model_name == model,
|
||||
ProviderModel.model_type == model_type.to_origin_model_type(),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
return provider_model_record
|
||||
|
||||
def custom_model_credentials_validate(
|
||||
self, model_type: ModelType, model: str, credentials: dict
|
||||
) -> tuple[ProviderModel | None, dict]:
|
||||
@ -361,16 +374,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:return:
|
||||
"""
|
||||
# get provider model
|
||||
provider_model_record = (
|
||||
db.session.query(ProviderModel)
|
||||
.filter(
|
||||
ProviderModel.tenant_id == self.tenant_id,
|
||||
ProviderModel.provider_name == self.provider.provider,
|
||||
ProviderModel.model_name == model,
|
||||
ProviderModel.model_type == model_type.to_origin_model_type(),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
provider_model_record = self._get_custom_model_credentials(model_type, model)
|
||||
|
||||
# Get provider credential secret variables
|
||||
provider_credential_secret_variables = self.extract_secret_variables(
|
||||
@ -451,16 +455,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:return:
|
||||
"""
|
||||
# get provider model
|
||||
provider_model_record = (
|
||||
db.session.query(ProviderModel)
|
||||
.filter(
|
||||
ProviderModel.tenant_id == self.tenant_id,
|
||||
ProviderModel.provider_name == self.provider.provider,
|
||||
ProviderModel.model_name == model,
|
||||
ProviderModel.model_type == model_type.to_origin_model_type(),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
provider_model_record = self._get_custom_model_credentials(model_type, model)
|
||||
|
||||
# delete provider model
|
||||
if provider_model_record:
|
||||
@ -475,6 +470,26 @@ class ProviderConfiguration(BaseModel):
|
||||
|
||||
provider_model_credentials_cache.delete()
|
||||
|
||||
def _get_provider_model_setting(self, model_type: ModelType, model: str) -> ProviderModelSetting | None:
|
||||
"""
|
||||
Get provider model setting.
|
||||
"""
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
return (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name.in_(provider_names),
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def enable_model(self, model_type: ModelType, model: str) -> ProviderModelSetting:
|
||||
"""
|
||||
Enable model.
|
||||
@ -482,16 +497,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
model_setting = (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
model_setting = self._get_provider_model_setting(model_type, model)
|
||||
|
||||
if model_setting:
|
||||
model_setting.enabled = True
|
||||
@ -516,16 +522,7 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
model_setting = (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
model_setting = self._get_provider_model_setting(model_type, model)
|
||||
|
||||
if model_setting:
|
||||
model_setting.enabled = False
|
||||
@ -550,13 +547,24 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
return self._get_provider_model_setting(model_type, model)
|
||||
|
||||
def _get_load_balancing_config(self, model_type: ModelType, model: str) -> Optional[LoadBalancingModelConfig]:
|
||||
"""
|
||||
Get load balancing config.
|
||||
"""
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
return (
|
||||
db.session.query(ProviderModelSetting)
|
||||
db.session.query(LoadBalancingModelConfig)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
LoadBalancingModelConfig.tenant_id == self.tenant_id,
|
||||
LoadBalancingModelConfig.provider_name.in_(provider_names),
|
||||
LoadBalancingModelConfig.model_type == model_type.to_origin_model_type(),
|
||||
LoadBalancingModelConfig.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
@ -568,11 +576,16 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
load_balancing_config_count = (
|
||||
db.session.query(LoadBalancingModelConfig)
|
||||
.filter(
|
||||
LoadBalancingModelConfig.tenant_id == self.tenant_id,
|
||||
LoadBalancingModelConfig.provider_name == self.provider.provider,
|
||||
LoadBalancingModelConfig.provider_name.in_(provider_names),
|
||||
LoadBalancingModelConfig.model_type == model_type.to_origin_model_type(),
|
||||
LoadBalancingModelConfig.model_name == model,
|
||||
)
|
||||
@ -582,16 +595,7 @@ class ProviderConfiguration(BaseModel):
|
||||
if load_balancing_config_count <= 1:
|
||||
raise ValueError("Model load balancing configuration must be more than 1.")
|
||||
|
||||
model_setting = (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
model_setting = self._get_provider_model_setting(model_type, model)
|
||||
|
||||
if model_setting:
|
||||
model_setting.load_balancing_enabled = True
|
||||
@ -616,11 +620,16 @@ class ProviderConfiguration(BaseModel):
|
||||
:param model: model name
|
||||
:return:
|
||||
"""
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
model_setting = (
|
||||
db.session.query(ProviderModelSetting)
|
||||
.filter(
|
||||
ProviderModelSetting.tenant_id == self.tenant_id,
|
||||
ProviderModelSetting.provider_name == self.provider.provider,
|
||||
ProviderModelSetting.provider_name.in_(provider_names),
|
||||
ProviderModelSetting.model_type == model_type.to_origin_model_type(),
|
||||
ProviderModelSetting.model_name == model,
|
||||
)
|
||||
@ -677,11 +686,16 @@ class ProviderConfiguration(BaseModel):
|
||||
return
|
||||
|
||||
# get preferred provider
|
||||
model_provider_id = ModelProviderID(self.provider.provider)
|
||||
provider_names = [self.provider.provider]
|
||||
if model_provider_id.is_langgenius():
|
||||
provider_names.append(model_provider_id.provider_name)
|
||||
|
||||
preferred_model_provider = (
|
||||
db.session.query(TenantPreferredModelProvider)
|
||||
.filter(
|
||||
TenantPreferredModelProvider.tenant_id == self.tenant_id,
|
||||
TenantPreferredModelProvider.provider_name == self.provider.provider,
|
||||
TenantPreferredModelProvider.provider_name.in_(provider_names),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@ -88,7 +88,10 @@ class FixedRecursiveCharacterTextSplitter(EnhanceRecursiveCharacterTextSplitter)
|
||||
break
|
||||
# Now that we have the separator, split the text
|
||||
if separator:
|
||||
splits = text.split(separator)
|
||||
if separator == " ":
|
||||
splits = text.split()
|
||||
else:
|
||||
splits = text.split(separator)
|
||||
else:
|
||||
splits = list(text)
|
||||
# Now go merging things, recursively splitting longer texts.
|
||||
|
||||
@ -179,6 +179,18 @@ class ApiTool(Tool):
|
||||
for content_type in self.api_bundle.openapi["requestBody"]["content"]:
|
||||
headers["Content-Type"] = content_type
|
||||
body_schema = self.api_bundle.openapi["requestBody"]["content"][content_type]["schema"]
|
||||
|
||||
# handle ref schema
|
||||
if "$ref" in body_schema:
|
||||
ref_path = body_schema["$ref"].split("/")
|
||||
ref_name = ref_path[-1]
|
||||
if (
|
||||
"components" in self.api_bundle.openapi
|
||||
and "schemas" in self.api_bundle.openapi["components"]
|
||||
):
|
||||
if ref_name in self.api_bundle.openapi["components"]["schemas"]:
|
||||
body_schema = self.api_bundle.openapi["components"]["schemas"][ref_name]
|
||||
|
||||
required = body_schema.get("required", [])
|
||||
properties = body_schema.get("properties", {})
|
||||
for name, property in properties.items():
|
||||
@ -186,6 +198,8 @@ class ApiTool(Tool):
|
||||
if property.get("format") == "binary":
|
||||
f = parameters[name]
|
||||
files.append((name, (f.filename, download(f), f.mime_type)))
|
||||
elif "$ref" in property:
|
||||
body[name] = parameters[name]
|
||||
else:
|
||||
# convert type
|
||||
body[name] = self._convert_body_property_type(property, parameters[name])
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import Any, cast
|
||||
|
||||
@ -19,8 +20,10 @@ from core.workflow.entities.node_entities import NodeRunResult
|
||||
from core.workflow.nodes.base import BaseNode
|
||||
from core.workflow.nodes.enums import NodeType
|
||||
from extensions.ext_database import db
|
||||
from models.dataset import Dataset, Document
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.dataset import Dataset, Document, RateLimitLog
|
||||
from models.workflow import WorkflowNodeExecutionStatus
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
from .entities import KnowledgeRetrievalNodeData
|
||||
from .exc import (
|
||||
@ -61,6 +64,31 @@ class KnowledgeRetrievalNode(BaseNode[KnowledgeRetrievalNodeData]):
|
||||
return NodeRunResult(
|
||||
status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, error="Query is required."
|
||||
)
|
||||
# check rate limit
|
||||
if self.tenant_id:
|
||||
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(self.tenant_id)
|
||||
if knowledge_rate_limit.enabled:
|
||||
current_time = int(time.time() * 1000)
|
||||
key = f"rate_limit_{self.tenant_id}"
|
||||
redis_client.zadd(key, {current_time: current_time})
|
||||
redis_client.zremrangebyscore(key, 0, current_time - 60000)
|
||||
request_count = redis_client.zcard(key)
|
||||
if request_count > knowledge_rate_limit.limit:
|
||||
# add ratelimit record
|
||||
rate_limit_log = RateLimitLog(
|
||||
tenant_id=self.tenant_id,
|
||||
subscription_plan=knowledge_rate_limit.subscription_plan,
|
||||
operation="knowledge",
|
||||
)
|
||||
db.session.add(rate_limit_log)
|
||||
db.session.commit()
|
||||
return NodeRunResult(
|
||||
status=WorkflowNodeExecutionStatus.FAILED,
|
||||
inputs=variables,
|
||||
error="Sorry, you have reached the knowledge base request rate limit of your subscription.",
|
||||
error_type="RateLimitExceeded",
|
||||
)
|
||||
|
||||
# retrieve knowledge
|
||||
try:
|
||||
results = self._fetch_dataset_retriever(node_data=self.node_data, query=query)
|
||||
|
||||
@ -94,6 +94,9 @@ class LLMNode(BaseNode[LLMNodeData]):
|
||||
def _run(self) -> Generator[NodeEvent | InNodeEvent, None, None]:
|
||||
node_inputs: Optional[dict[str, Any]] = None
|
||||
process_data = None
|
||||
result_text = ""
|
||||
usage = LLMUsage.empty_usage()
|
||||
finish_reason = None
|
||||
|
||||
try:
|
||||
# init messages template
|
||||
@ -178,9 +181,6 @@ class LLMNode(BaseNode[LLMNodeData]):
|
||||
stop=stop,
|
||||
)
|
||||
|
||||
result_text = ""
|
||||
usage = LLMUsage.empty_usage()
|
||||
finish_reason = None
|
||||
for event in generator:
|
||||
if isinstance(event, RunStreamChunkEvent):
|
||||
yield event
|
||||
|
||||
@ -270,7 +270,9 @@ class ToolNode(BaseNode[ToolNodeData]):
|
||||
if self.node_type == NodeType.AGENT:
|
||||
msg_metadata = message.message.json_object.pop("execution_metadata", {})
|
||||
agent_execution_metadata = {
|
||||
key: value for key, value in msg_metadata.items() if key in NodeRunMetadataKey
|
||||
key: value
|
||||
for key, value in msg_metadata.items()
|
||||
if key in NodeRunMetadataKey.__members__.values()
|
||||
}
|
||||
json.append(message.message.json_object)
|
||||
elif message.type == ToolInvokeMessage.MessageType.LINK:
|
||||
|
||||
@ -32,11 +32,7 @@ class AwsS3Storage(BaseStorage):
|
||||
aws_access_key_id=dify_config.S3_ACCESS_KEY,
|
||||
endpoint_url=dify_config.S3_ENDPOINT,
|
||||
region_name=dify_config.S3_REGION,
|
||||
config=Config(
|
||||
s3={"addressing_style": dify_config.S3_ADDRESS_STYLE},
|
||||
request_checksum_calculation="when_required",
|
||||
response_checksum_validation="when_required",
|
||||
),
|
||||
config=Config(s3={"addressing_style": dify_config.S3_ADDRESS_STYLE}),
|
||||
)
|
||||
# create bucket
|
||||
try:
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
"""add_rate_limit_logs
|
||||
|
||||
Revision ID: f051706725cc
|
||||
Revises: 923752d42eb6
|
||||
Create Date: 2025-01-14 06:17:35.536388
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import models as models
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f051706725cc'
|
||||
down_revision = 'ee79d9b1c156'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('rate_limit_logs',
|
||||
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
|
||||
sa.Column('subscription_plan', sa.String(length=255), nullable=False),
|
||||
sa.Column('operation', sa.String(length=255), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name='rate_limit_log_pkey')
|
||||
)
|
||||
with op.batch_alter_table('rate_limit_logs', schema=None) as batch_op:
|
||||
batch_op.create_index('rate_limit_log_operation_idx', ['operation'], unique=False)
|
||||
batch_op.create_index('rate_limit_log_tenant_idx', ['tenant_id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('rate_limit_logs', schema=None) as batch_op:
|
||||
batch_op.drop_index('rate_limit_log_tenant_idx')
|
||||
batch_op.drop_index('rate_limit_log_operation_idx')
|
||||
|
||||
op.drop_table('rate_limit_logs')
|
||||
# ### end Alembic commands ###
|
||||
@ -930,3 +930,18 @@ class DatasetAutoDisableLog(db.Model): # type: ignore[name-defined]
|
||||
document_id = db.Column(StringUUID, nullable=False)
|
||||
notified = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
|
||||
|
||||
class RateLimitLog(db.Model): # type: ignore[name-defined]
|
||||
__tablename__ = "rate_limit_logs"
|
||||
__table_args__ = (
|
||||
db.PrimaryKeyConstraint("id", name="rate_limit_log_pkey"),
|
||||
db.Index("rate_limit_log_tenant_idx", "tenant_id"),
|
||||
db.Index("rate_limit_log_operation_idx", "operation"),
|
||||
)
|
||||
|
||||
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
|
||||
tenant_id = db.Column(StringUUID, nullable=False)
|
||||
subscription_plan = db.Column(db.String(255), nullable=False)
|
||||
operation = db.Column(db.String(255), nullable=False)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)"))
|
||||
|
||||
@ -257,7 +257,7 @@ class App(Base):
|
||||
provider_id = tool.get("provider_id", "")
|
||||
|
||||
if provider_type == ToolProviderType.API.value:
|
||||
if provider_id not in existing_api_providers:
|
||||
if uuid.UUID(provider_id) not in existing_api_providers:
|
||||
deleted_tools.append(
|
||||
{
|
||||
"type": ToolProviderType.API.value,
|
||||
|
||||
@ -22,6 +22,17 @@ class BillingService:
|
||||
billing_info = cls._send_request("GET", "/subscription/info", params=params)
|
||||
return billing_info
|
||||
|
||||
@classmethod
|
||||
def get_knowledge_rate_limit(cls, tenant_id: str):
|
||||
params = {"tenant_id": tenant_id}
|
||||
|
||||
knowledge_rate_limit = cls._send_request("GET", "/subscription/knowledge-rate-limit", params=params)
|
||||
|
||||
return {
|
||||
"limit": knowledge_rate_limit.get("limit", 10),
|
||||
"subscription_plan": knowledge_rate_limit.get("subscription_plan", "sandbox"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_subscription(cls, plan: str, interval: str, prefilled_email: str = "", tenant_id: str = ""):
|
||||
params = {"plan": plan, "interval": interval, "prefilled_email": prefilled_email, "tenant_id": tenant_id}
|
||||
|
||||
@ -245,7 +245,7 @@ class DatasetService:
|
||||
"No Embedding Model available. Please configure a valid provider in the Settings -> Model Provider."
|
||||
)
|
||||
except ProviderTokenNotInitError as ex:
|
||||
raise ValueError(f"The dataset in unavailable, due to: {ex.description}")
|
||||
raise ValueError(ex.description)
|
||||
|
||||
@staticmethod
|
||||
def update_dataset(dataset_id, data, user):
|
||||
@ -327,31 +327,75 @@ class DatasetService:
|
||||
raise ValueError(ex.description)
|
||||
else:
|
||||
# add default plugin id to both setting sets, to make sure the plugin model provider is consistent
|
||||
plugin_model_provider = dataset.embedding_model_provider
|
||||
plugin_model_provider = str(ModelProviderID(plugin_model_provider))
|
||||
|
||||
new_plugin_model_provider = data["embedding_model_provider"]
|
||||
new_plugin_model_provider = str(ModelProviderID(new_plugin_model_provider))
|
||||
|
||||
# Skip embedding model checks if not provided in the update request
|
||||
if (
|
||||
new_plugin_model_provider != plugin_model_provider
|
||||
or data["embedding_model"] != dataset.embedding_model
|
||||
"embedding_model_provider" not in data
|
||||
or "embedding_model" not in data
|
||||
or not data.get("embedding_model_provider")
|
||||
or not data.get("embedding_model")
|
||||
):
|
||||
action = "update"
|
||||
# If the dataset already has embedding model settings, use those
|
||||
if dataset.embedding_model_provider and dataset.embedding_model:
|
||||
# Keep existing values
|
||||
filtered_data["embedding_model_provider"] = dataset.embedding_model_provider
|
||||
filtered_data["embedding_model"] = dataset.embedding_model
|
||||
# If collection_binding_id exists, keep it too
|
||||
if dataset.collection_binding_id:
|
||||
filtered_data["collection_binding_id"] = dataset.collection_binding_id
|
||||
# Otherwise, don't try to update embedding model settings at all
|
||||
# Remove these fields from filtered_data if they exist but are None/empty
|
||||
if "embedding_model_provider" in filtered_data and not filtered_data["embedding_model_provider"]:
|
||||
del filtered_data["embedding_model_provider"]
|
||||
if "embedding_model" in filtered_data and not filtered_data["embedding_model"]:
|
||||
del filtered_data["embedding_model"]
|
||||
else:
|
||||
skip_embedding_update = False
|
||||
try:
|
||||
model_manager = ModelManager()
|
||||
embedding_model = model_manager.get_model_instance(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
provider=data["embedding_model_provider"],
|
||||
model_type=ModelType.TEXT_EMBEDDING,
|
||||
model=data["embedding_model"],
|
||||
)
|
||||
filtered_data["embedding_model"] = embedding_model.model
|
||||
filtered_data["embedding_model_provider"] = embedding_model.provider
|
||||
dataset_collection_binding = DatasetCollectionBindingService.get_dataset_collection_binding(
|
||||
embedding_model.provider, embedding_model.model
|
||||
)
|
||||
filtered_data["collection_binding_id"] = dataset_collection_binding.id
|
||||
# Handle existing model provider
|
||||
plugin_model_provider = dataset.embedding_model_provider
|
||||
plugin_model_provider_str = None
|
||||
if plugin_model_provider:
|
||||
plugin_model_provider_str = str(ModelProviderID(plugin_model_provider))
|
||||
|
||||
# Handle new model provider from request
|
||||
new_plugin_model_provider = data["embedding_model_provider"]
|
||||
new_plugin_model_provider_str = None
|
||||
if new_plugin_model_provider:
|
||||
new_plugin_model_provider_str = str(ModelProviderID(new_plugin_model_provider))
|
||||
|
||||
# Only update embedding model if both values are provided and different from current
|
||||
if (
|
||||
plugin_model_provider_str != new_plugin_model_provider_str
|
||||
or data["embedding_model"] != dataset.embedding_model
|
||||
):
|
||||
action = "update"
|
||||
model_manager = ModelManager()
|
||||
try:
|
||||
embedding_model = model_manager.get_model_instance(
|
||||
tenant_id=current_user.current_tenant_id,
|
||||
provider=data["embedding_model_provider"],
|
||||
model_type=ModelType.TEXT_EMBEDDING,
|
||||
model=data["embedding_model"],
|
||||
)
|
||||
except ProviderTokenNotInitError:
|
||||
# If we can't get the embedding model, skip updating it
|
||||
# and keep the existing settings if available
|
||||
if dataset.embedding_model_provider and dataset.embedding_model:
|
||||
filtered_data["embedding_model_provider"] = dataset.embedding_model_provider
|
||||
filtered_data["embedding_model"] = dataset.embedding_model
|
||||
if dataset.collection_binding_id:
|
||||
filtered_data["collection_binding_id"] = dataset.collection_binding_id
|
||||
# Skip the rest of the embedding model update
|
||||
skip_embedding_update = True
|
||||
if not skip_embedding_update:
|
||||
filtered_data["embedding_model"] = embedding_model.model
|
||||
filtered_data["embedding_model_provider"] = embedding_model.provider
|
||||
dataset_collection_binding = (
|
||||
DatasetCollectionBindingService.get_dataset_collection_binding(
|
||||
embedding_model.provider, embedding_model.model
|
||||
)
|
||||
)
|
||||
filtered_data["collection_binding_id"] = dataset_collection_binding.id
|
||||
except LLMBadRequestError:
|
||||
raise ValueError(
|
||||
"No Embedding Model available. Please configure a valid provider "
|
||||
|
||||
@ -41,6 +41,7 @@ class FeatureModel(BaseModel):
|
||||
members: LimitationModel = LimitationModel(size=0, limit=1)
|
||||
apps: LimitationModel = LimitationModel(size=0, limit=10)
|
||||
vector_space: LimitationModel = LimitationModel(size=0, limit=5)
|
||||
knowledge_rate_limit: int = 10
|
||||
annotation_quota_limit: LimitationModel = LimitationModel(size=0, limit=10)
|
||||
documents_upload_quota: LimitationModel = LimitationModel(size=0, limit=50)
|
||||
docs_processing: str = "standard"
|
||||
@ -52,6 +53,12 @@ class FeatureModel(BaseModel):
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
|
||||
class KnowledgeRateLimitModel(BaseModel):
|
||||
enabled: bool = False
|
||||
limit: int = 10
|
||||
subscription_plan: str = ""
|
||||
|
||||
|
||||
class SystemFeatureModel(BaseModel):
|
||||
sso_enforced_for_signin: bool = False
|
||||
sso_enforced_for_signin_protocol: str = ""
|
||||
@ -81,6 +88,16 @@ class FeatureService:
|
||||
|
||||
return features
|
||||
|
||||
@classmethod
|
||||
def get_knowledge_rate_limit(cls, tenant_id: str):
|
||||
knowledge_rate_limit = KnowledgeRateLimitModel()
|
||||
if dify_config.BILLING_ENABLED and tenant_id:
|
||||
knowledge_rate_limit.enabled = True
|
||||
limit_info = BillingService.get_knowledge_rate_limit(tenant_id)
|
||||
knowledge_rate_limit.limit = limit_info.get("limit", 10)
|
||||
knowledge_rate_limit.subscription_plan = limit_info.get("subscription_plan", "sandbox")
|
||||
return knowledge_rate_limit
|
||||
|
||||
@classmethod
|
||||
def get_system_features(cls) -> SystemFeatureModel:
|
||||
system_features = SystemFeatureModel()
|
||||
@ -149,6 +166,9 @@ class FeatureService:
|
||||
if "model_load_balancing_enabled" in billing_info:
|
||||
features.model_load_balancing_enabled = billing_info["model_load_balancing_enabled"]
|
||||
|
||||
if "knowledge_rate_limit" in billing_info:
|
||||
features.knowledge_rate_limit = billing_info["knowledge_rate_limit"]["limit"]
|
||||
|
||||
@classmethod
|
||||
def _fulfill_params_from_enterprise(cls, features):
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
|
||||
@ -959,6 +959,7 @@ PLUGIN_DEBUGGING_PORT=5003
|
||||
EXPOSE_PLUGIN_DEBUGGING_HOST=localhost
|
||||
EXPOSE_PLUGIN_DEBUGGING_PORT=5003
|
||||
|
||||
# If this key is changed, DIFY_INNER_API_KEY in plugin_daemon service must also be updated or agent node will fail.
|
||||
PLUGIN_DIFY_INNER_API_KEY=QaHbTe77CtuXmsfyhR7+vRjI/+XbV1AaFy691iy+kGDv2Jvy0/eAh8Y1
|
||||
PLUGIN_DIFY_INNER_API_URL=http://api:5001
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:1.0.0
|
||||
image: langgenius/dify-api:1.0.1
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -29,7 +29,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:1.0.0
|
||||
image: langgenius/dify-api:1.0.1
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -53,7 +53,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:1.0.0
|
||||
image: langgenius/dify-web:1.0.1
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@ -131,7 +131,7 @@ services:
|
||||
|
||||
# plugin daemon
|
||||
plugin_daemon:
|
||||
image: langgenius/dify-plugin-daemon:0.0.3-local
|
||||
image: langgenius/dify-plugin-daemon:0.0.4-local
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
||||
@ -66,7 +66,7 @@ services:
|
||||
|
||||
# plugin daemon
|
||||
plugin_daemon:
|
||||
image: langgenius/dify-plugin-daemon:0.0.3-local
|
||||
image: langgenius/dify-plugin-daemon:0.0.4-local
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
||||
@ -420,7 +420,7 @@ x-shared-env: &shared-api-worker-env
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:1.0.0
|
||||
image: langgenius/dify-api:1.0.1
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -447,7 +447,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:1.0.0
|
||||
image: langgenius/dify-api:1.0.1
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -471,7 +471,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:1.0.0
|
||||
image: langgenius/dify-web:1.0.1
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@ -549,7 +549,7 @@ services:
|
||||
|
||||
# plugin daemon
|
||||
plugin_daemon:
|
||||
image: langgenius/dify-plugin-daemon:0.0.3-local
|
||||
image: langgenius/dify-plugin-daemon:0.0.4-local
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
|
||||
@ -33,7 +33,6 @@ import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/co
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import VersionInfoModal from './version-info-modal'
|
||||
|
||||
export type AppPublisherProps = {
|
||||
disabled?: boolean
|
||||
@ -45,6 +44,7 @@ export type AppPublisherProps = {
|
||||
multipleModelConfigs?: ModelAndParameter[]
|
||||
/** modelAndParameter is passed when debugWithMultipleModel is true */
|
||||
onPublish?: (params?: any) => Promise<any> | any
|
||||
onRestore?: () => Promise<any> | any
|
||||
onToggle?: (state: boolean) => void
|
||||
crossAxisOffset?: number
|
||||
toolPublished?: boolean
|
||||
@ -62,6 +62,7 @@ const AppPublisher = ({
|
||||
debugWithMultipleModel = false,
|
||||
multipleModelConfigs = [],
|
||||
onPublish,
|
||||
onRestore,
|
||||
onToggle,
|
||||
crossAxisOffset = 0,
|
||||
toolPublished,
|
||||
@ -71,18 +72,18 @@ const AppPublisher = ({
|
||||
const { t } = useTranslation()
|
||||
const [published, setPublished] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [publishModalOpen, setPublishModalOpen] = useState(false)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
|
||||
const appURL = `${appBaseURL}/${appMode}/${accessToken}`
|
||||
const isChatApp = ['chat', 'agent-chat', 'completion'].includes(appDetail?.mode || '')
|
||||
|
||||
const language = useGetLanguage()
|
||||
const formatTimeFromNow = useCallback((time: number) => {
|
||||
return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
|
||||
}, [language])
|
||||
|
||||
const handlePublish = async (params?: ModelAndParameter | PublishWorkflowParams) => {
|
||||
const handlePublish = useCallback(async (params?: ModelAndParameter | PublishWorkflowParams) => {
|
||||
try {
|
||||
await onPublish?.(params)
|
||||
setPublished(true)
|
||||
@ -90,7 +91,15 @@ const AppPublisher = ({
|
||||
catch {
|
||||
setPublished(false)
|
||||
}
|
||||
}
|
||||
}, [onPublish])
|
||||
|
||||
const handleRestore = useCallback(async () => {
|
||||
try {
|
||||
await onRestore?.()
|
||||
setOpen(false)
|
||||
}
|
||||
catch {}
|
||||
}, [onRestore])
|
||||
|
||||
const handleTrigger = useCallback(() => {
|
||||
const state = !open
|
||||
@ -122,20 +131,11 @@ const AppPublisher = ({
|
||||
|
||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||
|
||||
const openPublishModal = () => {
|
||||
setOpen(false)
|
||||
setPublishModalOpen(true)
|
||||
}
|
||||
|
||||
const closePublishModal = () => {
|
||||
setPublishModalOpen(false)
|
||||
}
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
|
||||
e.preventDefault()
|
||||
if (publishDisabled || published)
|
||||
return
|
||||
openPublishModal()
|
||||
handlePublish()
|
||||
}
|
||||
, { exactMatch: true, useCapture: true })
|
||||
|
||||
@ -168,8 +168,18 @@ const AppPublisher = ({
|
||||
</div>
|
||||
{publishedAt
|
||||
? (
|
||||
<div className='flex items-center system-sm-medium text-text-secondary'>
|
||||
{t('workflow.common.publishedAt')} {formatTimeFromNow(publishedAt)}
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center system-sm-medium text-text-secondary'>
|
||||
{t('workflow.common.publishedAt')} {formatTimeFromNow(publishedAt)}
|
||||
</div>
|
||||
{isChatApp && <Button
|
||||
variant='secondary-accent'
|
||||
size='small'
|
||||
onClick={handleRestore}
|
||||
disabled={published}
|
||||
>
|
||||
{t('workflow.common.restore')}
|
||||
</Button>}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
@ -189,7 +199,7 @@ const AppPublisher = ({
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full mt-3'
|
||||
onClick={openPublishModal}
|
||||
onClick={() => handlePublish()}
|
||||
disabled={publishDisabled || published}
|
||||
>
|
||||
{
|
||||
@ -286,13 +296,6 @@ const AppPublisher = ({
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
</PortalToFollowElem >
|
||||
{publishModalOpen && (
|
||||
<VersionInfoModal
|
||||
isOpen={publishModalOpen}
|
||||
onClose={closePublishModal}
|
||||
onPublish={handlePublish}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
</div>
|
||||
<Input
|
||||
value={title}
|
||||
placeholder={t('workflow.versionHistory.nameThisVersion')}
|
||||
placeholder={`${t('workflow.versionHistory.nameThisVersion')}${t('workflow.panel.optional')}`}
|
||||
onChange={handleTitleChange}
|
||||
destructive={titleError}
|
||||
/>
|
||||
@ -94,7 +94,7 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
</div>
|
||||
<Textarea
|
||||
value={releaseNotes}
|
||||
placeholder={t('workflow.versionHistory.releaseNotesPlaceholder')}
|
||||
placeholder={`${t('workflow.versionHistory.releaseNotesPlaceholder')}${t('workflow.panel.optional')}`}
|
||||
onChange={handleDescriptionChange}
|
||||
destructive={releaseNotesError}
|
||||
/>
|
||||
|
||||
@ -11,7 +11,7 @@ const GroupName: FC<IGroupNameProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex items-center mb-1'>
|
||||
<div className='mr-3 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{name}</div>
|
||||
<div className='mr-3 leading-[18px] text-xs font-semibold text-text-tertiary uppercase'>{name}</div>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
Clipboard,
|
||||
ClipboardCheck,
|
||||
} from '@/app/components/base/icons/src/vender/line/files'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
@ -150,19 +151,20 @@ const AdvancedPromptInput: FC<Props> = ({
|
||||
<RiErrorWarningFill className='mr-1 w-4 h-4 text-[#F79009]' />
|
||||
<div className='leading-[18px] text-[13px] font-medium text-[#DC6803]'>{t('appDebug.promptMode.contextMissing')}</div>
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center h-6 px-2 rounded-md bg-[#fff] border border-gray-200 shadow-xs text-xs font-medium text-primary-600 cursor-pointer'
|
||||
<Button
|
||||
size='small'
|
||||
variant='secondary-accent'
|
||||
onClick={onHideContextMissingTip}
|
||||
>{t('common.operation.ok')}</div>
|
||||
>{t('common.operation.ok')}</Button>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className={`relative ${!isContextMissing ? s.gradientBorder : s.warningBorder}`}>
|
||||
<div className='rounded-xl bg-white'>
|
||||
<div className={`bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2 rounded-xl p-0.5 shadow-xs ${!isContextMissing ? '' : s.warningBorder}`}>
|
||||
<div className='rounded-xl bg-background-default'>
|
||||
{isContextMissing
|
||||
? contextMissing
|
||||
: (
|
||||
<div className={cn(s.boxHeader, 'flex justify-between items-center h-11 pt-2 pr-3 pb-1 pl-4 rounded-tl-xl rounded-tr-xl bg-white hover:shadow-xs')}>
|
||||
<div className={cn(s.boxHeader, 'flex justify-between items-center h-11 pt-2 pr-3 pb-1 pl-4 rounded-tl-xl rounded-tr-xl bg-background-default hover:shadow-xs')}>
|
||||
{isChatMode
|
||||
? (
|
||||
<MessageTypeSelector value={type} onChange={onTypeChange} />
|
||||
@ -182,30 +184,30 @@ const AdvancedPromptInput: FC<Props> = ({
|
||||
</div>)}
|
||||
<div className={cn(s.optionWrap, 'items-center space-x-1')}>
|
||||
{canDelete && (
|
||||
<RiDeleteBinLine onClick={onDelete} className='h-6 w-6 p-1 text-gray-500 cursor-pointer' />
|
||||
<RiDeleteBinLine onClick={onDelete} className='h-6 w-6 p-1 text-text-tertiary cursor-pointer' />
|
||||
)}
|
||||
{!isCopied
|
||||
? (
|
||||
<Clipboard className='h-6 w-6 p-1 text-gray-500 cursor-pointer' onClick={() => {
|
||||
<Clipboard className='h-6 w-6 p-1 text-text-tertiary cursor-pointer' onClick={() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}} />
|
||||
)
|
||||
: (
|
||||
<ClipboardCheck className='h-6 w-6 p-1 text-gray-500' />
|
||||
<ClipboardCheck className='h-6 w-6 p-1 text-text-tertiary' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<PromptEditorHeightResizeWrap
|
||||
className='px-4 min-h-[102px] overflow-y-auto text-sm text-gray-700'
|
||||
className='px-4 min-h-[102px] overflow-y-auto text-sm text-text-secondary'
|
||||
height={editorHeight}
|
||||
minHeight={minHeight}
|
||||
onHeightChange={setEditorHeight}
|
||||
footer={(
|
||||
<div className='pl-4 pb-2 flex'>
|
||||
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{value.length}</div>
|
||||
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-divider-regular text-xs text-text-tertiary">{value.length}</div>
|
||||
</div>
|
||||
)}
|
||||
hideResize={noResize}
|
||||
|
||||
@ -39,21 +39,17 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
|
||||
}}>
|
||||
<div
|
||||
ref={mainContentRef}
|
||||
className='w-[420px] rounded-xl bg-gray-50 p-6'
|
||||
className='w-[420px] rounded-xl bg-components-panel-bg p-6'
|
||||
style={{
|
||||
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
|
||||
}}
|
||||
>
|
||||
<div className='flex items-start space-x-3'>
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center h-10 w-10 rounded-xl border border-gray-100'
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
|
||||
}}
|
||||
className='shrink-0 flex items-center justify-center h-10 w-10 bg-components-card-bg-alt rounded-xl border border-components-card-border shadow-lg'
|
||||
>{VarIcon}</div>
|
||||
<div className='grow-1'>
|
||||
<div className='text-sm font-medium text-gray-900'>{t('appDebug.autoAddVar')}</div>
|
||||
<div className='text-sm font-medium text-text-primary'>{t('appDebug.autoAddVar')}</div>
|
||||
<div className='flex flex-wrap mt-[15px] max-h-[66px] overflow-y-auto px-1 space-x-1'>
|
||||
{varNameArr.map(name => (
|
||||
<VarHighlight key={name} name={name} />
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SimplePromptInput from './simple-prompt-input'
|
||||
import Button from '@/app/components/base/button'
|
||||
import AdvancedMessageInput from '@/app/components/app/configuration/config-prompt/advanced-prompt-input'
|
||||
import { PromptRole } from '@/models/debug'
|
||||
import type { PromptItem, PromptVariable } from '@/models/debug'
|
||||
@ -155,12 +156,12 @@ const Prompt: FC<IPromptProps> = ({
|
||||
}
|
||||
</div>
|
||||
{(modelModeType === ModelModeType.chat && (currentAdvancedPrompt as PromptItem[]).length < MAX_PROMPT_MESSAGE_LENGTH) && (
|
||||
<div
|
||||
<Button
|
||||
onClick={handleAddMessage}
|
||||
className='mt-3 flex items-center h-8 justify-center bg-gray-50 rounded-lg cursor-pointer text-[13px] font-medium text-gray-700 space-x-2'>
|
||||
<RiAddLine className='w-4 h-4' />
|
||||
className='mt-3 w-full'>
|
||||
<RiAddLine className='w-4 h-4 mr-2' />
|
||||
<div>{t('appDebug.promptMode.operation.addMessage')}</div>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -29,7 +29,7 @@ const MessageTypeSelector: FC<Props> = ({
|
||||
<ChevronSelectorVertical className='w-3 h-3 ' />
|
||||
</div>
|
||||
{showOption && (
|
||||
<div className='absolute z-10 top-[30px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white'>
|
||||
<div className='absolute z-10 top-[30px] p-1 border border-components-panel-border shadow-lg rounded-lg bg-components-panel-bg'>
|
||||
{allTypes.map(type => (
|
||||
<div
|
||||
key={type}
|
||||
@ -37,7 +37,7 @@ const MessageTypeSelector: FC<Props> = ({
|
||||
setHide()
|
||||
onChange(type)
|
||||
}}
|
||||
className='flex items-center h-9 min-w-[44px] px-3 rounded-lg cursor-pointer text-sm font-medium text-gray-700 uppercase hover:bg-gray-50'
|
||||
className='flex items-center h-9 min-w-[44px] px-3 rounded-lg cursor-pointer text-sm font-medium text-text-secondary uppercase hover:bg-state-base-hover'
|
||||
>{type}</div>
|
||||
))
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import { useBoolean } from 'ahooks'
|
||||
import produce from 'immer'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ConfirmAddVar from './confirm-add-var'
|
||||
import s from './style.module.css'
|
||||
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
@ -48,7 +47,6 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
readonly = false,
|
||||
onChange,
|
||||
noTitle,
|
||||
gradientBorder,
|
||||
editorHeight: initEditorHeight,
|
||||
noResize,
|
||||
}) => {
|
||||
@ -161,12 +159,12 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
const [editorHeight, setEditorHeight] = useState(minHeight)
|
||||
|
||||
return (
|
||||
<div className={cn((!readonly || gradientBorder) ? `${s.gradientBorder}` : 'bg-gray-50', ' relative shadow-md')}>
|
||||
<div className='rounded-xl bg-[#EEF4FF]'>
|
||||
<div className={cn('relative bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2 rounded-xl p-0.5 shadow-xs')}>
|
||||
<div className='rounded-xl bg-background-section-burn'>
|
||||
{!noTitle && (
|
||||
<div className="flex justify-between items-center h-11 pl-3 pr-6">
|
||||
<div className="flex justify-between items-center h-11 pl-3 pr-2.5">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className='h2'>{mode !== AppType.completion ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
|
||||
<div className='h2 system-sm-semibold-uppercase text-text-secondary'>{mode !== AppType.completion ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
|
||||
{!readonly && (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
@ -186,14 +184,14 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
)}
|
||||
|
||||
<PromptEditorHeightResizeWrap
|
||||
className='px-4 pt-2 min-h-[228px] bg-white rounded-t-xl text-sm text-gray-700'
|
||||
className='px-4 pt-2 min-h-[228px] bg-background-default rounded-t-xl text-sm text-text-secondary'
|
||||
height={editorHeight}
|
||||
minHeight={minHeight}
|
||||
onHeightChange={setEditorHeight}
|
||||
hideResize={noResize}
|
||||
footer={(
|
||||
<div className='pl-4 pb-2 flex bg-white rounded-b-xl'>
|
||||
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{promptTemplate.length}</div>
|
||||
<div className='pl-4 pb-2 flex bg-background-default rounded-b-xl'>
|
||||
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-components-badge-bg-gray-soft text-xs text-text-tertiary">{promptTemplate.length}</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
|
||||
@ -2,7 +2,10 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
import {
|
||||
RiSparklingFill,
|
||||
} from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type IAutomaticBtnProps = {
|
||||
onClick: () => void
|
||||
@ -13,12 +16,10 @@ const AutomaticBtn: FC<IAutomaticBtnProps> = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex space-x-1 items-center !h-8 cursor-pointer'
|
||||
onClick={onClick}
|
||||
>
|
||||
<Generator className='w-3.5 h-3.5 text-indigo-600' />
|
||||
<span className='text-xs font-semibold text-indigo-600'>{t('appDebug.operation.automatic')}</span>
|
||||
</div>
|
||||
<Button variant='secondary-accent' size='small' onClick={onClick}>
|
||||
<RiSparklingFill className='w-3.5 h-3.5 mr-1' />
|
||||
<span className=''>{t('appDebug.operation.automatic')}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
export default React.memo(AutomaticBtn)
|
||||
|
||||
@ -38,7 +38,7 @@ import ModelName from '@/app/components/header/account-setting/model-provider-pa
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
||||
export interface IGetAutomaticResProps {
|
||||
export type IGetAutomaticResProps = {
|
||||
mode: AppType
|
||||
model: Model
|
||||
isShow: boolean
|
||||
@ -54,11 +54,11 @@ const TryLabel: FC<{
|
||||
}> = ({ Icon, text, onClick }) => {
|
||||
return (
|
||||
<div
|
||||
className='mt-2 mr-1 shrink-0 flex h-7 items-center px-2 bg-gray-100 rounded-lg cursor-pointer'
|
||||
className='mt-2 mr-1 shrink-0 flex h-7 items-center px-2 bg-components-button-secondary-bg rounded-lg cursor-pointer'
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon className='w-4 h-4 text-gray-500'></Icon>
|
||||
<div className='ml-1 text-xs font-medium text-gray-700'>{text}</div>
|
||||
<Icon className='w-4 h-4 text-text-tertiary'></Icon>
|
||||
<div className='ml-1 text-xs font-medium text-text-secondary'>{text}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -140,14 +140,14 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
const renderLoading = (
|
||||
<div className='w-0 grow flex flex-col items-center justify-center h-full space-y-3'>
|
||||
<Loading />
|
||||
<div className='text-[13px] text-gray-400'>{t('appDebug.generate.loading')}</div>
|
||||
<div className='text-[13px] text-text-tertiary'>{t('appDebug.generate.loading')}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderNoData = (
|
||||
<div className='w-0 grow flex flex-col items-center px-8 justify-center h-full space-y-3'>
|
||||
<Generator className='w-14 h-14 text-gray-300' />
|
||||
<div className='leading-5 text-center text-[13px] font-normal text-gray-400'>
|
||||
<Generator className='w-14 h-14 text-text-tertiary' />
|
||||
<div className='leading-5 text-center text-[13px] font-normal text-text-tertiary'>
|
||||
<div>{t('appDebug.generate.noDataLine1')}</div>
|
||||
<div>{t('appDebug.generate.noDataLine2')}</div>
|
||||
</div>
|
||||
@ -193,10 +193,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
closable
|
||||
>
|
||||
<div className='flex h-[680px] flex-wrap'>
|
||||
<div className='w-[570px] shrink-0 p-6 h-full overflow-y-auto border-r border-gray-100'>
|
||||
<div className='w-[570px] shrink-0 p-6 h-full overflow-y-auto border-r border-divider-regular'>
|
||||
<div className='mb-8'>
|
||||
<div className={`leading-[28px] text-lg font-bold ${s.textGradient}`}>{t('appDebug.generate.title')}</div>
|
||||
<div className='mt-1 text-[13px] font-normal text-gray-500'>{t('appDebug.generate.description')}</div>
|
||||
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div>
|
||||
</div>
|
||||
<div className='flex items-center mb-8'>
|
||||
<ModelIcon
|
||||
@ -213,7 +213,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
</div>
|
||||
<div >
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-3 shrink-0 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appDebug.generate.tryIt')}</div>
|
||||
<div className='mr-3 shrink-0 leading-[18px] text-xs font-semibold text-text-tertiary uppercase'>{t('appDebug.generate.tryIt')}</div>
|
||||
<div className='grow h-px' style={{
|
||||
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
|
||||
}}></div>
|
||||
@ -232,7 +232,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
{/* inputs */}
|
||||
<div className='mt-6'>
|
||||
<div className='text-[0px]'>
|
||||
<div className='mb-2 leading-5 text-sm font-medium text-gray-900'>{t('appDebug.generate.instruction')}</div>
|
||||
<div className='mb-2 leading-5 text-sm font-medium text-text-primary'>{t('appDebug.generate.instruction')}</div>
|
||||
<Textarea
|
||||
className="h-[200px] resize-none"
|
||||
placeholder={t('appDebug.generate.instructionPlaceHolder') as string}
|
||||
@ -256,7 +256,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
|
||||
{(!isLoading && res) && (
|
||||
<div className='w-0 grow p-6 pb-0 h-full'>
|
||||
<div className='shrink-0 mb-3 leading-[160%] text-base font-semibold text-gray-800'>{t('appDebug.generate.resTitle')}</div>
|
||||
<div className='shrink-0 mb-3 leading-[160%] text-base font-semibold text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
|
||||
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
|
||||
<ConfigPrompt
|
||||
mode={mode}
|
||||
@ -301,7 +301,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex justify-end py-4 bg-white'>
|
||||
<div className='flex justify-end py-4 bg-background-default'>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' className='ml-2' onClick={() => {
|
||||
setShowConfirmOverwrite(true)
|
||||
|
||||
@ -56,7 +56,7 @@ const WorkflowProcessItem = ({
|
||||
>
|
||||
{
|
||||
running && (
|
||||
<RiLoader2Line className='shrink-0 mr-1 w-3.5 h-3.5 text-text-tertiary' />
|
||||
<RiLoader2Line className='shrink-0 mr-1 w-3.5 h-3.5 animate-spin text-text-tertiary' />
|
||||
)
|
||||
}
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@ import mermaid from 'mermaid'
|
||||
import { usePrevious } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
||||
import { cleanUpSvgCode } from './utils'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import cn from '@/utils/classnames'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
@ -44,7 +45,7 @@ const Flowchart = React.forwardRef((props: {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && mermaidAPI) {
|
||||
const svgGraph = await mermaidAPI.render('flowchart', PrimitiveCode)
|
||||
const base64Svg: any = await svgToBase64(svgGraph.svg)
|
||||
const base64Svg: any = await svgToBase64(cleanUpSvgCode(svgGraph.svg))
|
||||
setSvgCode(base64Svg)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
8
web/app/components/base/mermaid/utils.spec.ts
Normal file
8
web/app/components/base/mermaid/utils.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { cleanUpSvgCode } from './utils'
|
||||
|
||||
describe('cleanUpSvgCode', () => {
|
||||
it('replaces old-style <br> tags with the new style', () => {
|
||||
const result = cleanUpSvgCode('<br>test<br>')
|
||||
expect(result).toEqual('<br/>test<br/>')
|
||||
})
|
||||
})
|
||||
3
web/app/components/base/mermaid/utils.ts
Normal file
3
web/app/components/base/mermaid/utils.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function cleanUpSvgCode(svgCode: string): string {
|
||||
return svgCode.replaceAll('<br>', '<br/>')
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
@layer components {
|
||||
.premium-badge {
|
||||
@apply inline-flex justify-center items-center rounded-full border box-border border-[rgba(255,255,255,0.8)] text-white
|
||||
@apply inline-flex justify-center items-center rounded-md border box-border border-white/95 text-white
|
||||
}
|
||||
|
||||
/* m is for the regular button */
|
||||
|
||||
@ -62,7 +62,7 @@ const PremiumBadge: React.FC<PremiumBadgeProps> = ({
|
||||
<Highlight
|
||||
className={classNames(
|
||||
'absolute top-0 opacity-50 hover:opacity-80',
|
||||
size === 's' ? 'h-4.5 w-12' : 'h-6 w-12',
|
||||
size === 's' ? 'h-[18px] w-12' : 'h-6 w-12',
|
||||
)}
|
||||
style={{
|
||||
right: '50%',
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
.modal {
|
||||
max-width: 480px !important;
|
||||
width: 480px !important;
|
||||
padding: 24px 32px !important;
|
||||
}
|
||||
@ -3,9 +3,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import dayjs from 'dayjs'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import s from './index.module.css'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { LangGeniusVersionResponse } from '@/models/common'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
@ -29,18 +28,18 @@ export default function AccountAbout({
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => { }}
|
||||
className={s.modal}
|
||||
className='!w-[480px] !max-w-[480px] !px-6 !py-4'
|
||||
>
|
||||
<div className='relative pt-4'>
|
||||
<div className='absolute -top-2 -right-4 flex justify-center items-center w-8 h-8 cursor-pointer' onClick={onCancel}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div>
|
||||
<LogoSite className='mx-auto mb-2' />
|
||||
<div className='mb-3 text-center text-xs font-normal text-gray-500'>Version {langeniusVersionInfo?.current_version}</div>
|
||||
<div className='mb-4 text-center text-xs font-normal text-gray-700'>
|
||||
<div className='mb-3 text-center text-xs font-normal text-text-tertiary'>Version {langeniusVersionInfo?.current_version}</div>
|
||||
<div className='mb-4 text-center text-xs font-normal text-text-secondary'>
|
||||
<div>© {dayjs().year()} LangGenius, Inc., Contributors.</div>
|
||||
<div className='text-[#1C64F2]'>
|
||||
<div className='text-text-accent'>
|
||||
{
|
||||
IS_CE_EDITION
|
||||
? <Link href={'https://github.com/langgenius/dify/blob/main/LICENSE'} target='_blank' rel='noopener noreferrer'>Open Source License</Link>
|
||||
@ -52,9 +51,9 @@ export default function AccountAbout({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-4 -mx-8 h-[0.5px] bg-gray-200' />
|
||||
<div className='mb-4 -mx-8 h-[0.5px] bg-divider-regular' />
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='text-xs font-medium text-gray-800'>
|
||||
<div className='text-xs font-medium text-text-primary'>
|
||||
{
|
||||
isLatest
|
||||
? t('common.about.latestAvailable', { version: langeniusVersionInfo.latest_version })
|
||||
@ -62,22 +61,24 @@ export default function AccountAbout({
|
||||
}
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Link
|
||||
className={classNames(buttonClassName, 'mr-2')}
|
||||
href={'https://github.com/langgenius/dify/releases'}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
>
|
||||
{t('common.about.changeLog')}
|
||||
</Link>
|
||||
<Button className='mr-2'>
|
||||
<Link
|
||||
href={'https://github.com/langgenius/dify/releases'}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
>
|
||||
{t('common.about.changeLog')}
|
||||
</Link>
|
||||
</Button>
|
||||
{
|
||||
!isLatest && !IS_CE_EDITION && (
|
||||
<Link
|
||||
className={classNames(buttonClassName, 'text-primary-600')}
|
||||
href={langeniusVersionInfo.release_notes}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
>
|
||||
{t('common.about.updateNow')}
|
||||
</Link>
|
||||
<Button variant='primary'>
|
||||
<Link
|
||||
href={langeniusVersionInfo.release_notes}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
>
|
||||
{t('common.about.updateNow')}
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -7,6 +7,8 @@ import cn from '@/utils/classnames'
|
||||
import { switchWorkspace } from '@/service/common'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import PlanBadge from '../../plan-badge'
|
||||
import type { Plan } from '@/app/components/billing/type'
|
||||
|
||||
const WorkplaceSelector = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -69,6 +71,7 @@ const WorkplaceSelector = () => {
|
||||
<div className='flex py-1 pl-3 pr-2 items-center gap-2 self-stretch hover:bg-state-base-hover rounded-lg' key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}>
|
||||
<div className='flex items-center justify-center w-6 h-6 bg-[#EFF4FF] rounded-md text-xs font-medium text-primary-600'>{workspace.name[0].toLocaleUpperCase()}</div>
|
||||
<div className='line-clamp-1 grow overflow-hidden text-text-secondary text-ellipsis system-md-regular cursor-pointer'>{workspace.name}</div>
|
||||
<PlanBadge size='s' plan={workspace.plan as Plan} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
@ -20,49 +20,41 @@ const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, size = 'm', sandboxAs
|
||||
|
||||
if (!isFetchedPlan) return null
|
||||
if (plan === Plan.sandbox && sandboxAsUpgrade) {
|
||||
return <div className='select-none'>
|
||||
<PremiumBadge color='blue' allowHover={allowHover} onClick={onClick}>
|
||||
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
|
||||
<div className='system-xs-medium'>
|
||||
<span className='p-1'>
|
||||
{t('billing.upgradeBtn.encourageShort')}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
return <PremiumBadge className='select-none' color='blue' allowHover={allowHover} onClick={onClick}>
|
||||
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
|
||||
<div className='system-xs-medium'>
|
||||
<span className='p-1'>
|
||||
{t('billing.upgradeBtn.encourageShort')}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
}
|
||||
if (plan === Plan.sandbox) {
|
||||
return <div className='select-none'>
|
||||
<PremiumBadge size={size} color='gray' allowHover={allowHover} onClick={onClick}>
|
||||
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
|
||||
<span className='p-1'>
|
||||
{plan}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
return <PremiumBadge className='select-none' size={size} color='gray' allowHover={allowHover} onClick={onClick}>
|
||||
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
|
||||
<span className='p-1'>
|
||||
{plan}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
}
|
||||
if (plan === Plan.professional) {
|
||||
return <div className='select-none'>
|
||||
<PremiumBadge size={size} color='blue' allowHover={allowHover} onClick={onClick}>
|
||||
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
|
||||
<span className='p-1'>
|
||||
pro
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
return <PremiumBadge className='select-none' size={size} color='blue' allowHover={allowHover} onClick={onClick}>
|
||||
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
|
||||
<span className='p-1'>
|
||||
pro
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
}
|
||||
if (plan === Plan.team) {
|
||||
return <div className='select-none'>
|
||||
<PremiumBadge size={size} color='indigo' allowHover={allowHover} onClick={onClick}>
|
||||
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
|
||||
<span className='p-1'>
|
||||
{plan}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
</div>
|
||||
return <PremiumBadge className='select-none' size={size} color='indigo' allowHover={allowHover} onClick={onClick}>
|
||||
<div className={classNames(size === 's' ? 'system-2xs-medium-uppercase' : 'system-xs-medium-uppercase')}>
|
||||
<span className='p-1'>
|
||||
{plan}
|
||||
</span>
|
||||
</div>
|
||||
</PremiumBadge>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@ -109,6 +109,7 @@ const Item: FC<ItemProps> = ({
|
||||
'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer')
|
||||
}
|
||||
onClick={handleChosen}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
>
|
||||
<div className='flex items-center w-0 grow'>
|
||||
{!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
|
||||
Reference in New Issue
Block a user