mirror of
https://github.com/langgenius/dify.git
synced 2026-01-09 07:44:12 +00:00
Compare commits
19 Commits
feat/oauth
...
feat/model
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aa5273c5e | ||
|
|
473b465efb | ||
|
|
2e28a64d38 | ||
|
|
3f57e4a643 | ||
|
|
4e02abf784 | ||
|
|
61be6f5d2c | ||
|
|
e69797d738 | ||
|
|
415178fb0d | ||
|
|
1a642084b5 | ||
|
|
d9ccd74f0b | ||
|
|
4e6cb26778 | ||
|
|
12083de2ab | ||
|
|
3522eb51b6 | ||
|
|
8e1ea671bd | ||
|
|
d2eda60e0e | ||
|
|
b73487dd67 | ||
|
|
7ad64bfb60 | ||
|
|
1aec17f912 | ||
|
|
c1c7a43191 |
3
.github/workflows/autofix.yml
vendored
3
.github/workflows/autofix.yml
vendored
@@ -23,9 +23,6 @@ jobs:
|
||||
uv run ruff check --fix-only .
|
||||
# Format code
|
||||
uv run ruff format .
|
||||
- name: ast-grep
|
||||
run: |
|
||||
uvx --from ast-grep-cli sg --pattern 'db.session.query($WHATEVER).filter($HERE)' --rewrite 'db.session.query($WHATEVER).where($HERE)' -l py --update-all
|
||||
|
||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||
|
||||
|
||||
@@ -478,13 +478,6 @@ API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node
|
||||
|
||||
# API workflow run repository implementation
|
||||
API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository
|
||||
# Workflow log cleanup configuration
|
||||
# Enable automatic cleanup of workflow run logs to manage database size
|
||||
WORKFLOW_LOG_CLEANUP_ENABLED=true
|
||||
# Number of days to retain workflow run logs (default: 30 days)
|
||||
WORKFLOW_LOG_RETENTION_DAYS=30
|
||||
# Batch size for workflow log cleanup operations (default: 100)
|
||||
WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100
|
||||
|
||||
# App configuration
|
||||
APP_MAX_EXECUTION_TIME=1200
|
||||
|
||||
@@ -968,14 +968,6 @@ class AccountConfig(BaseSettings):
|
||||
)
|
||||
|
||||
|
||||
class WorkflowLogConfig(BaseSettings):
|
||||
WORKFLOW_LOG_CLEANUP_ENABLED: bool = Field(default=True, description="Enable workflow run log cleanup")
|
||||
WORKFLOW_LOG_RETENTION_DAYS: int = Field(default=30, description="Retention days for workflow run logs")
|
||||
WORKFLOW_LOG_CLEANUP_BATCH_SIZE: int = Field(
|
||||
default=100, description="Batch size for workflow run log cleanup operations"
|
||||
)
|
||||
|
||||
|
||||
class FeatureConfig(
|
||||
# place the configs in alphabet order
|
||||
AppExecutionConfig,
|
||||
@@ -1011,6 +1003,5 @@ class FeatureConfig(
|
||||
HostedServiceConfig,
|
||||
CeleryBeatConfig,
|
||||
CeleryScheduleTasksConfig,
|
||||
WorkflowLogConfig,
|
||||
):
|
||||
pass
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import contextlib
|
||||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
@@ -66,8 +65,10 @@ def guess_file_info_from_response(response: httpx.Response):
|
||||
|
||||
# Use python-magic to guess MIME type if still unknown or generic
|
||||
if mimetype == "application/octet-stream" and magic is not None:
|
||||
with contextlib.suppress(magic.MagicException):
|
||||
try:
|
||||
mimetype = magic.from_buffer(response.content[:1024], mime=True)
|
||||
except magic.MagicException:
|
||||
pass
|
||||
|
||||
extension = os.path.splitext(filename)[1]
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ from .app import (
|
||||
)
|
||||
|
||||
# Import auth controllers
|
||||
from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth, oauth_server
|
||||
from .auth import activate, data_source_bearer_auth, data_source_oauth, forgot_password, login, oauth
|
||||
|
||||
# Import billing controllers
|
||||
from .billing import billing, compliance
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Literal
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
from flask_restful import Resource, marshal, marshal_with, reqparse
|
||||
@@ -26,7 +24,7 @@ class AnnotationReplyActionApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("annotation")
|
||||
def post(self, app_id, action: Literal["enable", "disable"]):
|
||||
def post(self, app_id, action):
|
||||
if not current_user.is_editor:
|
||||
raise Forbidden()
|
||||
|
||||
@@ -40,6 +38,8 @@ class AnnotationReplyActionApi(Resource):
|
||||
result = AppAnnotationService.enable_app_annotation(args, app_id)
|
||||
elif action == "disable":
|
||||
result = AppAnnotationService.disable_app_annotation(app_id)
|
||||
else:
|
||||
raise ValueError("Unsupported annotation reply action")
|
||||
return result, 200
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from controllers.console.app.error import (
|
||||
)
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.errors.error import ModelCurrentlyNotSupportError, ProviderTokenNotInitError, QuotaExceededError
|
||||
from core.helper.code_executor.code_node_provider import CodeNodeProvider
|
||||
from core.helper.code_executor.javascript.javascript_code_provider import JavascriptCodeProvider
|
||||
from core.helper.code_executor.python3.python3_code_provider import Python3CodeProvider
|
||||
from core.llm_generator.llm_generator import LLMGenerator
|
||||
@@ -125,20 +126,18 @@ class InstructionGenerateApi(Resource):
|
||||
parser.add_argument("model_config", type=dict, required=True, nullable=False, location="json")
|
||||
parser.add_argument("ideal_output", type=str, required=False, default="", location="json")
|
||||
args = parser.parse_args()
|
||||
code_template = (
|
||||
Python3CodeProvider.get_default_code()
|
||||
if args["language"] == "python"
|
||||
else (JavascriptCodeProvider.get_default_code())
|
||||
if args["language"] == "javascript"
|
||||
else ""
|
||||
providers: list[type[CodeNodeProvider]] = [Python3CodeProvider, JavascriptCodeProvider]
|
||||
code_provider: type[CodeNodeProvider] | None = next(
|
||||
(p for p in providers if p.is_accept_language(args["language"])), None
|
||||
)
|
||||
code_template = code_provider.get_default_code() if code_provider else ""
|
||||
try:
|
||||
# Generate from nothing for a workflow node
|
||||
if (args["current"] == code_template or args["current"] == "") and args["node_id"] != "":
|
||||
from models import App, db
|
||||
from services.workflow_service import WorkflowService
|
||||
|
||||
app = db.session.query(App).where(App.id == args["flow_id"]).first()
|
||||
app = db.session.query(App).filter(App.id == args["flow_id"]).first()
|
||||
if not app:
|
||||
return {"error": f"app {args['flow_id']} not found"}, 400
|
||||
workflow = WorkflowService().get_draft_workflow(app_model=app)
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
from functools import wraps
|
||||
from typing import cast
|
||||
|
||||
import flask_login
|
||||
from flask import request
|
||||
from flask_restful import Resource, reqparse
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.model_runtime.utils.encoders import jsonable_encoder
|
||||
from libs.login import login_required
|
||||
from models.account import Account
|
||||
from models.model import OAuthProviderApp
|
||||
from services.oauth_server import OAUTH_ACCESS_TOKEN_EXPIRES_IN, OAuthGrantType, OAuthServerService
|
||||
|
||||
from .. import api
|
||||
|
||||
|
||||
def oauth_server_client_id_required(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("client_id", type=str, required=True, location="json")
|
||||
parsed_args = parser.parse_args()
|
||||
client_id = parsed_args.get("client_id")
|
||||
if not client_id:
|
||||
raise BadRequest("client_id is required")
|
||||
|
||||
oauth_provider_app = OAuthServerService.get_oauth_provider_app(client_id)
|
||||
if not oauth_provider_app:
|
||||
raise NotFound("client_id is invalid")
|
||||
|
||||
kwargs["oauth_provider_app"] = oauth_provider_app
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def oauth_server_access_token_required(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
oauth_provider_app = kwargs.get("oauth_provider_app")
|
||||
if not oauth_provider_app or not isinstance(oauth_provider_app, OAuthProviderApp):
|
||||
raise BadRequest("Invalid oauth_provider_app")
|
||||
|
||||
if not request.headers.get("Authorization"):
|
||||
raise BadRequest("Authorization is required")
|
||||
|
||||
authorization_header = request.headers.get("Authorization")
|
||||
if not authorization_header:
|
||||
raise BadRequest("Authorization header is required")
|
||||
|
||||
parts = authorization_header.split(" ")
|
||||
if len(parts) != 2:
|
||||
raise BadRequest("Invalid Authorization header format")
|
||||
|
||||
token_type = parts[0]
|
||||
if token_type != "Bearer":
|
||||
raise BadRequest("token_type is invalid")
|
||||
|
||||
access_token = parts[1]
|
||||
if not access_token:
|
||||
raise BadRequest("access_token is required")
|
||||
|
||||
account = OAuthServerService.validate_oauth_access_token(oauth_provider_app.client_id, access_token)
|
||||
if not account:
|
||||
raise BadRequest("access_token or client_id is invalid")
|
||||
|
||||
kwargs["account"] = account
|
||||
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
class OAuthServerAppApi(Resource):
|
||||
@setup_required
|
||||
@oauth_server_client_id_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("redirect_uri", type=str, required=True, location="json")
|
||||
parsed_args = parser.parse_args()
|
||||
redirect_uri = parsed_args.get("redirect_uri")
|
||||
|
||||
# check if redirect_uri is valid
|
||||
if redirect_uri not in oauth_provider_app.redirect_uris:
|
||||
raise BadRequest("redirect_uri is invalid")
|
||||
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"app_icon": oauth_provider_app.app_icon,
|
||||
"app_label": oauth_provider_app.app_label,
|
||||
"scope": oauth_provider_app.scope,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OAuthServerUserAuthorizeApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@oauth_server_client_id_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp):
|
||||
account = cast(Account, flask_login.current_user)
|
||||
user_account_id = account.id
|
||||
|
||||
code = OAuthServerService.sign_oauth_authorization_code(oauth_provider_app.client_id, user_account_id)
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"code": code,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OAuthServerUserTokenApi(Resource):
|
||||
@setup_required
|
||||
@oauth_server_client_id_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("grant_type", type=str, required=True, location="json")
|
||||
parser.add_argument("code", type=str, required=False, location="json")
|
||||
parser.add_argument("client_secret", type=str, required=False, location="json")
|
||||
parser.add_argument("redirect_uri", type=str, required=False, location="json")
|
||||
parser.add_argument("refresh_token", type=str, required=False, location="json")
|
||||
parsed_args = parser.parse_args()
|
||||
|
||||
grant_type = OAuthGrantType(parsed_args["grant_type"])
|
||||
|
||||
if grant_type == OAuthGrantType.AUTHORIZATION_CODE:
|
||||
if not parsed_args["code"]:
|
||||
raise BadRequest("code is required")
|
||||
|
||||
if parsed_args["client_secret"] != oauth_provider_app.client_secret:
|
||||
raise BadRequest("client_secret is invalid")
|
||||
|
||||
if parsed_args["redirect_uri"] not in oauth_provider_app.redirect_uris:
|
||||
raise BadRequest("redirect_uri is invalid")
|
||||
|
||||
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
|
||||
grant_type, code=parsed_args["code"], client_id=oauth_provider_app.client_id
|
||||
)
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
)
|
||||
elif grant_type == OAuthGrantType.REFRESH_TOKEN:
|
||||
if not parsed_args["refresh_token"]:
|
||||
raise BadRequest("refresh_token is required")
|
||||
|
||||
access_token, refresh_token = OAuthServerService.sign_oauth_access_token(
|
||||
grant_type, refresh_token=parsed_args["refresh_token"], client_id=oauth_provider_app.client_id
|
||||
)
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": OAUTH_ACCESS_TOKEN_EXPIRES_IN,
|
||||
"refresh_token": refresh_token,
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise BadRequest("invalid grant_type")
|
||||
|
||||
|
||||
class OAuthServerUserAccountApi(Resource):
|
||||
@setup_required
|
||||
@oauth_server_client_id_required
|
||||
@oauth_server_access_token_required
|
||||
def post(self, oauth_provider_app: OAuthProviderApp, account: Account):
|
||||
return jsonable_encoder(
|
||||
{
|
||||
"name": account.name,
|
||||
"email": account.email,
|
||||
"avatar": account.avatar,
|
||||
"interface_language": account.interface_language,
|
||||
"timezone": account.timezone,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
api.add_resource(OAuthServerAppApi, "/oauth/provider")
|
||||
api.add_resource(OAuthServerUserAuthorizeApi, "/oauth/provider/authorize")
|
||||
api.add_resource(OAuthServerUserTokenApi, "/oauth/provider/token")
|
||||
api.add_resource(OAuthServerUserAccountApi, "/oauth/provider/account")
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from argparse import ArgumentTypeError
|
||||
from typing import Literal, cast
|
||||
from typing import cast
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
@@ -758,7 +758,7 @@ class DocumentProcessingApi(DocumentResource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, document_id, action: Literal["pause", "resume"]):
|
||||
def patch(self, dataset_id, document_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
document_id = str(document_id)
|
||||
document = self.get_document(dataset_id, document_id)
|
||||
@@ -784,6 +784,8 @@ class DocumentProcessingApi(DocumentResource):
|
||||
document.paused_at = None
|
||||
document.is_paused = False
|
||||
db.session.commit()
|
||||
else:
|
||||
raise InvalidActionError()
|
||||
|
||||
return {"result": "success"}, 200
|
||||
|
||||
@@ -838,7 +840,7 @@ class DocumentStatusApi(DocumentResource):
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check("vector_space")
|
||||
@cloud_edition_billing_rate_limit_check("knowledge")
|
||||
def patch(self, dataset_id, action: Literal["enable", "disable", "archive", "un_archive"]):
|
||||
def patch(self, dataset_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
if dataset is None:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Literal
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_restful import Resource, marshal_with, reqparse
|
||||
from werkzeug.exceptions import NotFound
|
||||
@@ -102,7 +100,7 @@ class DatasetMetadataBuiltInFieldActionApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@enterprise_license_required
|
||||
def post(self, dataset_id, action: Literal["enable", "disable"]):
|
||||
def post(self, dataset_id, action):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
|
||||
@@ -39,7 +39,7 @@ class UploadFileApi(Resource):
|
||||
data_source_info = document.data_source_info_dict
|
||||
if data_source_info and "upload_file_id" in data_source_info:
|
||||
file_id = data_source_info["upload_file_id"]
|
||||
upload_file = db.session.query(UploadFile).where(UploadFile.id == file_id).first()
|
||||
upload_file = db.session.query(UploadFile).filter(UploadFile.id == file_id).first()
|
||||
if not upload_file:
|
||||
raise NotFound("UploadFile not found.")
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Literal
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource, marshal, marshal_with, reqparse
|
||||
from werkzeug.exceptions import Forbidden
|
||||
@@ -17,7 +15,7 @@ from services.annotation_service import AppAnnotationService
|
||||
|
||||
class AnnotationReplyActionApi(Resource):
|
||||
@validate_app_token
|
||||
def post(self, app_model: App, action: Literal["enable", "disable"]):
|
||||
def post(self, app_model: App, action):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("score_threshold", required=True, type=float, location="json")
|
||||
parser.add_argument("embedding_provider_name", required=True, type=str, location="json")
|
||||
@@ -27,6 +25,8 @@ class AnnotationReplyActionApi(Resource):
|
||||
result = AppAnnotationService.enable_app_annotation(args, app_model.id)
|
||||
elif action == "disable":
|
||||
result = AppAnnotationService.disable_app_annotation(app_model.id)
|
||||
else:
|
||||
raise ValueError("Unsupported annotation reply action")
|
||||
return result, 200
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Literal
|
||||
|
||||
from flask import request
|
||||
from flask_restful import marshal, marshal_with, reqparse
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
@@ -360,14 +358,14 @@ class DatasetApi(DatasetApiResource):
|
||||
class DocumentStatusApi(DatasetApiResource):
|
||||
"""Resource for batch document status operations."""
|
||||
|
||||
def patch(self, tenant_id, dataset_id, action: Literal["enable", "disable", "archive", "un_archive"]):
|
||||
def patch(self, tenant_id, dataset_id, action):
|
||||
"""
|
||||
Batch update document status.
|
||||
|
||||
Args:
|
||||
tenant_id: tenant id
|
||||
dataset_id: dataset id
|
||||
action: action to perform (Literal["enable", "disable", "archive", "un_archive"])
|
||||
action: action to perform (enable, disable, archive, un_archive)
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with a key 'result' and a value 'success'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Literal
|
||||
|
||||
from flask_login import current_user # type: ignore
|
||||
from flask_restful import marshal, reqparse
|
||||
from werkzeug.exceptions import NotFound
|
||||
@@ -79,7 +77,7 @@ class DatasetMetadataBuiltInFieldServiceApi(DatasetApiResource):
|
||||
|
||||
class DatasetMetadataBuiltInFieldActionServiceApi(DatasetApiResource):
|
||||
@cloud_edition_billing_rate_limit_check("knowledge", "dataset")
|
||||
def post(self, tenant_id, dataset_id, action: Literal["enable", "disable"]):
|
||||
def post(self, tenant_id, dataset_id, action):
|
||||
dataset_id_str = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||
if dataset is None:
|
||||
|
||||
@@ -181,7 +181,7 @@ class MessageCycleManager:
|
||||
:param message_id: message id
|
||||
:return:
|
||||
"""
|
||||
message_file = db.session.query(MessageFile).where(MessageFile.id == message_id).first()
|
||||
message_file = db.session.query(MessageFile).filter(MessageFile.id == message_id).first()
|
||||
event_type = StreamEvent.MESSAGE_FILE if message_file else StreamEvent.MESSAGE
|
||||
|
||||
return MessageStreamResponse(
|
||||
|
||||
@@ -399,9 +399,9 @@ class LLMGenerator:
|
||||
def instruction_modify_legacy(
|
||||
tenant_id: str, flow_id: str, current: str, instruction: str, model_config: dict, ideal_output: str | None
|
||||
) -> dict:
|
||||
app: App | None = db.session.query(App).where(App.id == flow_id).first()
|
||||
app: App | None = db.session.query(App).filter(App.id == flow_id).first()
|
||||
last_run: Message | None = (
|
||||
db.session.query(Message).where(Message.app_id == flow_id).order_by(Message.created_at.desc()).first()
|
||||
db.session.query(Message).filter(Message.app_id == flow_id).order_by(Message.created_at.desc()).first()
|
||||
)
|
||||
if not last_run:
|
||||
return LLMGenerator.__instruction_modify_common(
|
||||
@@ -442,7 +442,7 @@ class LLMGenerator:
|
||||
) -> dict:
|
||||
from services.workflow_service import WorkflowService
|
||||
|
||||
app: App | None = db.session.query(App).where(App.id == flow_id).first()
|
||||
app: App | None = db.session.query(App).filter(App.id == flow_id).first()
|
||||
if not app:
|
||||
raise ValueError("App not found.")
|
||||
workflow = WorkflowService().get_draft_workflow(app_model=app)
|
||||
|
||||
@@ -414,7 +414,7 @@ When you are modifying the code, you should remember:
|
||||
- Get inputs from the parameters of the function and have explicit type annotations.
|
||||
- Write proper imports at the top of the code.
|
||||
- Use return statement to return the result.
|
||||
- You should return a `dict`. If you need to return a `result: str`, you should `return {"result": result}`.
|
||||
- You should return a `dict`.
|
||||
Your output must strictly follow the schema format, do not output any content outside of the JSON body.
|
||||
""" # noqa: E501
|
||||
|
||||
|
||||
@@ -151,13 +151,7 @@ def init_app(app: DifyApp) -> Celery:
|
||||
"task": "schedule.check_upgradable_plugin_task.check_upgradable_plugin_task",
|
||||
"schedule": crontab(minute="*/15"),
|
||||
}
|
||||
if dify_config.WORKFLOW_LOG_CLEANUP_ENABLED:
|
||||
# 2:00 AM every day
|
||||
imports.append("schedule.clean_workflow_runlogs_precise")
|
||||
beat_schedule["clean_workflow_runlogs_precise"] = {
|
||||
"task": "schedule.clean_workflow_runlogs_precise.clean_workflow_runlogs_precise",
|
||||
"schedule": crontab(minute="0", hour="2"),
|
||||
}
|
||||
|
||||
celery_app.conf.update(beat_schedule=beat_schedule, imports=imports)
|
||||
|
||||
return celery_app
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 8d289573e1da
|
||||
Revises: fa8b0fa6f407
|
||||
Create Date: 2025-08-20 17:47:17.015695
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import models as models
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8d289573e1da'
|
||||
down_revision = 'fa8b0fa6f407'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('oauth_provider_apps',
|
||||
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('app_icon', sa.String(length=255), nullable=False),
|
||||
sa.Column('app_label', sa.JSON(), server_default='{}', nullable=False),
|
||||
sa.Column('client_id', sa.String(length=255), nullable=False),
|
||||
sa.Column('client_secret', sa.String(length=255), nullable=False),
|
||||
sa.Column('redirect_uris', sa.JSON(), server_default='[]', nullable=False),
|
||||
sa.Column('scope', sa.String(length=255), server_default=sa.text("'read:name read:email read:avatar read:interface_language read:timezone'"), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP(0)'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name='oauth_provider_app_pkey')
|
||||
)
|
||||
with op.batch_alter_table('oauth_provider_apps', schema=None) as batch_op:
|
||||
batch_op.create_index('oauth_provider_app_client_id_idx', ['client_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('oauth_provider_apps', schema=None) as batch_op:
|
||||
batch_op.drop_index('oauth_provider_app_client_id_idx')
|
||||
|
||||
op.drop_table('oauth_provider_apps')
|
||||
# ### end Alembic commands ###
|
||||
@@ -607,32 +607,6 @@ class InstalledApp(Base):
|
||||
return tenant
|
||||
|
||||
|
||||
class OAuthProviderApp(Base):
|
||||
"""
|
||||
Globally shared OAuth provider app information.
|
||||
Only for Dify Cloud.
|
||||
"""
|
||||
|
||||
__tablename__ = "oauth_provider_apps"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="oauth_provider_app_pkey"),
|
||||
sa.Index("oauth_provider_app_client_id_idx", "client_id"),
|
||||
)
|
||||
|
||||
id = mapped_column(StringUUID, server_default=sa.text("uuid_generate_v4()"))
|
||||
app_icon = mapped_column(String(255), nullable=False)
|
||||
app_label = mapped_column(sa.JSON, nullable=False, server_default="{}")
|
||||
client_id = mapped_column(String(255), nullable=False)
|
||||
client_secret = mapped_column(String(255), nullable=False)
|
||||
redirect_uris = mapped_column(sa.JSON, nullable=False, server_default="[]")
|
||||
scope = mapped_column(
|
||||
String(255),
|
||||
nullable=False,
|
||||
server_default=sa.text("'read:name read:email read:avatar read:interface_language read:timezone'"),
|
||||
)
|
||||
created_at = mapped_column(sa.DateTime, nullable=False, server_default=sa.text("CURRENT_TIMESTAMP(0)"))
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
__tablename__ = "conversations"
|
||||
__table_args__ = (
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
import click
|
||||
|
||||
import app
|
||||
from configs import dify_config
|
||||
from extensions.ext_database import db
|
||||
from models.model import (
|
||||
AppAnnotationHitHistory,
|
||||
Conversation,
|
||||
Message,
|
||||
MessageAgentThought,
|
||||
MessageAnnotation,
|
||||
MessageChain,
|
||||
MessageFeedback,
|
||||
MessageFile,
|
||||
)
|
||||
from models.workflow import ConversationVariable, WorkflowAppLog, WorkflowNodeExecutionModel, WorkflowRun
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
MAX_RETRIES = 3
|
||||
BATCH_SIZE = dify_config.WORKFLOW_LOG_CLEANUP_BATCH_SIZE
|
||||
|
||||
|
||||
@app.celery.task(queue="dataset")
|
||||
def clean_workflow_runlogs_precise():
|
||||
"""Clean expired workflow run logs with retry mechanism and complete message cascade"""
|
||||
|
||||
click.echo(click.style("Start clean workflow run logs (precise mode with complete cascade).", fg="green"))
|
||||
start_at = time.perf_counter()
|
||||
|
||||
retention_days = dify_config.WORKFLOW_LOG_RETENTION_DAYS
|
||||
cutoff_date = datetime.datetime.now() - datetime.timedelta(days=retention_days)
|
||||
|
||||
try:
|
||||
total_workflow_runs = db.session.query(WorkflowRun).where(WorkflowRun.created_at < cutoff_date).count()
|
||||
if total_workflow_runs == 0:
|
||||
_logger.info("No expired workflow run logs found")
|
||||
return
|
||||
_logger.info("Found %s expired workflow run logs to clean", total_workflow_runs)
|
||||
|
||||
total_deleted = 0
|
||||
failed_batches = 0
|
||||
batch_count = 0
|
||||
|
||||
while True:
|
||||
workflow_runs = (
|
||||
db.session.query(WorkflowRun.id).where(WorkflowRun.created_at < cutoff_date).limit(BATCH_SIZE).all()
|
||||
)
|
||||
|
||||
if not workflow_runs:
|
||||
break
|
||||
|
||||
workflow_run_ids = [run.id for run in workflow_runs]
|
||||
batch_count += 1
|
||||
|
||||
success = _delete_batch_with_retry(workflow_run_ids, failed_batches)
|
||||
|
||||
if success:
|
||||
total_deleted += len(workflow_run_ids)
|
||||
failed_batches = 0
|
||||
else:
|
||||
failed_batches += 1
|
||||
if failed_batches >= MAX_RETRIES:
|
||||
_logger.error("Failed to delete batch after %s retries, aborting cleanup for today", MAX_RETRIES)
|
||||
break
|
||||
else:
|
||||
# Calculate incremental delay times: 5, 10, 15 minutes
|
||||
retry_delay_minutes = failed_batches * 5
|
||||
_logger.warning("Batch deletion failed, retrying in %s minutes...", retry_delay_minutes)
|
||||
time.sleep(retry_delay_minutes * 60)
|
||||
continue
|
||||
|
||||
_logger.info("Cleanup completed: %s expired workflow run logs deleted", total_deleted)
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
_logger.exception("Unexpected error in workflow log cleanup")
|
||||
raise
|
||||
|
||||
end_at = time.perf_counter()
|
||||
execution_time = end_at - start_at
|
||||
click.echo(click.style(f"Cleaned workflow run logs from db success latency: {execution_time:.2f}s", fg="green"))
|
||||
|
||||
|
||||
def _delete_batch_with_retry(workflow_run_ids: list[str], attempt_count: int) -> bool:
|
||||
"""Delete a single batch with a retry mechanism and complete cascading deletion"""
|
||||
try:
|
||||
with db.session.begin_nested():
|
||||
message_data = (
|
||||
db.session.query(Message.id, Message.conversation_id)
|
||||
.filter(Message.workflow_run_id.in_(workflow_run_ids))
|
||||
.all()
|
||||
)
|
||||
message_id_list = [msg.id for msg in message_data]
|
||||
conversation_id_list = list({msg.conversation_id for msg in message_data if msg.conversation_id})
|
||||
if message_id_list:
|
||||
db.session.query(AppAnnotationHitHistory).where(
|
||||
AppAnnotationHitHistory.message_id.in_(message_id_list)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
db.session.query(MessageAgentThought).where(MessageAgentThought.message_id.in_(message_id_list)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.query(MessageChain).where(MessageChain.message_id.in_(message_id_list)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.query(MessageFile).where(MessageFile.message_id.in_(message_id_list)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.query(MessageAnnotation).where(MessageAnnotation.message_id.in_(message_id_list)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.query(MessageFeedback).where(MessageFeedback.message_id.in_(message_id_list)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.query(Message).where(Message.workflow_run_id.in_(workflow_run_ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.query(WorkflowAppLog).where(WorkflowAppLog.workflow_run_id.in_(workflow_run_ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.query(WorkflowNodeExecutionModel).where(
|
||||
WorkflowNodeExecutionModel.workflow_run_id.in_(workflow_run_ids)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
if conversation_id_list:
|
||||
db.session.query(ConversationVariable).where(
|
||||
ConversationVariable.conversation_id.in_(conversation_id_list)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
db.session.query(Conversation).where(Conversation.id.in_(conversation_id_list)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.query(WorkflowRun).where(WorkflowRun.id.in_(workflow_run_ids)).delete(synchronize_session=False)
|
||||
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
_logger.exception("Batch deletion failed (attempt %s)", attempt_count + 1)
|
||||
return False
|
||||
@@ -293,7 +293,7 @@ class AppAnnotationService:
|
||||
annotation_ids_to_delete = [annotation.id for annotation, _ in annotations_to_delete]
|
||||
|
||||
# Step 2: Bulk delete hit histories in a single query
|
||||
db.session.query(AppAnnotationHitHistory).where(
|
||||
db.session.query(AppAnnotationHitHistory).filter(
|
||||
AppAnnotationHitHistory.annotation_id.in_(annotation_ids_to_delete)
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
@@ -307,7 +307,7 @@ class AppAnnotationService:
|
||||
# Step 4: Bulk delete annotations in a single query
|
||||
deleted_count = (
|
||||
db.session.query(MessageAnnotation)
|
||||
.where(MessageAnnotation.id.in_(annotation_ids_to_delete))
|
||||
.filter(MessageAnnotation.id.in_(annotation_ids_to_delete))
|
||||
.delete(synchronize_session=False)
|
||||
)
|
||||
|
||||
@@ -505,9 +505,9 @@ class AppAnnotationService:
|
||||
db.session.query(AppAnnotationSetting).where(AppAnnotationSetting.app_id == app_id).first()
|
||||
)
|
||||
|
||||
annotations_query = db.session.query(MessageAnnotation).where(MessageAnnotation.app_id == app_id)
|
||||
annotations_query = db.session.query(MessageAnnotation).filter(MessageAnnotation.app_id == app_id)
|
||||
for annotation in annotations_query.yield_per(100):
|
||||
annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).where(
|
||||
annotation_hit_histories_query = db.session.query(AppAnnotationHitHistory).filter(
|
||||
AppAnnotationHitHistory.annotation_id == annotation.id
|
||||
)
|
||||
for annotation_hit_history in annotation_hit_histories_query.yield_per(100):
|
||||
|
||||
@@ -6,7 +6,7 @@ import secrets
|
||||
import time
|
||||
import uuid
|
||||
from collections import Counter
|
||||
from typing import Any, Literal, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import func, select
|
||||
@@ -51,7 +51,7 @@ from services.entities.knowledge_entities.knowledge_entities import (
|
||||
RetrievalModel,
|
||||
SegmentUpdateArgs,
|
||||
)
|
||||
from services.errors.account import NoPermissionError
|
||||
from services.errors.account import InvalidActionError, NoPermissionError
|
||||
from services.errors.chunk import ChildChunkDeleteIndexError, ChildChunkIndexingError
|
||||
from services.errors.dataset import DatasetNameDuplicateError
|
||||
from services.errors.document import DocumentIndexingError
|
||||
@@ -1800,16 +1800,14 @@ class DocumentService:
|
||||
raise ValueError("Process rule segmentation max_tokens is invalid")
|
||||
|
||||
@staticmethod
|
||||
def batch_update_document_status(
|
||||
dataset: Dataset, document_ids: list[str], action: Literal["enable", "disable", "archive", "un_archive"], user
|
||||
):
|
||||
def batch_update_document_status(dataset: Dataset, document_ids: list[str], action: str, user):
|
||||
"""
|
||||
Batch update document status.
|
||||
|
||||
Args:
|
||||
dataset (Dataset): The dataset object
|
||||
document_ids (list[str]): List of document IDs to update
|
||||
action (Literal["enable", "disable", "archive", "un_archive"]): Action to perform
|
||||
action (str): Action to perform (enable, disable, archive, un_archive)
|
||||
user: Current user performing the action
|
||||
|
||||
Raises:
|
||||
@@ -1892,10 +1890,9 @@ class DocumentService:
|
||||
raise propagation_error
|
||||
|
||||
@staticmethod
|
||||
def _prepare_document_status_update(
|
||||
document: Document, action: Literal["enable", "disable", "archive", "un_archive"], user
|
||||
):
|
||||
"""Prepare document status update information.
|
||||
def _prepare_document_status_update(document, action: str, user):
|
||||
"""
|
||||
Prepare document status update information.
|
||||
|
||||
Args:
|
||||
document: Document object to update
|
||||
@@ -2358,9 +2355,7 @@ class SegmentService:
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def update_segments_status(
|
||||
cls, segment_ids: list, action: Literal["enable", "disable"], dataset: Dataset, document: Document
|
||||
):
|
||||
def update_segments_status(cls, segment_ids: list, action: str, dataset: Dataset, document: Document):
|
||||
# Check if segment_ids is not empty to avoid WHERE false condition
|
||||
if not segment_ids or len(segment_ids) == 0:
|
||||
return
|
||||
@@ -2418,6 +2413,8 @@ class SegmentService:
|
||||
db.session.commit()
|
||||
|
||||
disable_segments_from_index_task.delay(real_deal_segment_ids, dataset.id, document.id)
|
||||
else:
|
||||
raise InvalidActionError()
|
||||
|
||||
@classmethod
|
||||
def create_child_chunk(
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import enum
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.account import Account
|
||||
from models.model import OAuthProviderApp
|
||||
from services.account_service import AccountService
|
||||
|
||||
|
||||
class OAuthGrantType(enum.StrEnum):
|
||||
AUTHORIZATION_CODE = "authorization_code"
|
||||
REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
|
||||
OAUTH_AUTHORIZATION_CODE_REDIS_KEY = "oauth_provider:{client_id}:authorization_code:{code}"
|
||||
OAUTH_ACCESS_TOKEN_REDIS_KEY = "oauth_provider:{client_id}:access_token:{token}"
|
||||
OAUTH_ACCESS_TOKEN_EXPIRES_IN = 60 * 60 * 12 # 12 hours
|
||||
OAUTH_REFRESH_TOKEN_REDIS_KEY = "oauth_provider:{client_id}:refresh_token:{token}"
|
||||
OAUTH_REFRESH_TOKEN_EXPIRES_IN = 60 * 60 * 24 * 30 # 30 days
|
||||
|
||||
|
||||
class OAuthServerService:
|
||||
@staticmethod
|
||||
def get_oauth_provider_app(client_id: str) -> OAuthProviderApp | None:
|
||||
query = select(OAuthProviderApp).where(OAuthProviderApp.client_id == client_id)
|
||||
|
||||
with Session(db.engine) as session:
|
||||
return session.execute(query).scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
def sign_oauth_authorization_code(client_id: str, user_account_id: str) -> str:
|
||||
code = str(uuid.uuid4())
|
||||
redis_key = OAUTH_AUTHORIZATION_CODE_REDIS_KEY.format(client_id=client_id, code=code)
|
||||
redis_client.set(redis_key, user_account_id, ex=60 * 10) # 10 minutes
|
||||
return code
|
||||
|
||||
@staticmethod
|
||||
def sign_oauth_access_token(
|
||||
grant_type: OAuthGrantType,
|
||||
code: str = "",
|
||||
client_id: str = "",
|
||||
refresh_token: str = "",
|
||||
) -> tuple[str, str]:
|
||||
match grant_type:
|
||||
case OAuthGrantType.AUTHORIZATION_CODE:
|
||||
redis_key = OAUTH_AUTHORIZATION_CODE_REDIS_KEY.format(client_id=client_id, code=code)
|
||||
user_account_id = redis_client.get(redis_key)
|
||||
if not user_account_id:
|
||||
raise BadRequest("invalid code")
|
||||
|
||||
# delete code
|
||||
redis_client.delete(redis_key)
|
||||
|
||||
access_token = OAuthServerService._sign_oauth_access_token(client_id, user_account_id)
|
||||
refresh_token = OAuthServerService._sign_oauth_refresh_token(client_id, user_account_id)
|
||||
return access_token, refresh_token
|
||||
case OAuthGrantType.REFRESH_TOKEN:
|
||||
redis_key = OAUTH_REFRESH_TOKEN_REDIS_KEY.format(client_id=client_id, token=refresh_token)
|
||||
user_account_id = redis_client.get(redis_key)
|
||||
if not user_account_id:
|
||||
raise BadRequest("invalid refresh token")
|
||||
|
||||
access_token = OAuthServerService._sign_oauth_access_token(client_id, user_account_id)
|
||||
return access_token, refresh_token
|
||||
|
||||
@staticmethod
|
||||
def _sign_oauth_access_token(client_id: str, user_account_id: str) -> str:
|
||||
token = str(uuid.uuid4())
|
||||
redis_key = OAUTH_ACCESS_TOKEN_REDIS_KEY.format(client_id=client_id, token=token)
|
||||
redis_client.set(redis_key, user_account_id, ex=OAUTH_ACCESS_TOKEN_EXPIRES_IN)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
def _sign_oauth_refresh_token(client_id: str, user_account_id: str) -> str:
|
||||
token = str(uuid.uuid4())
|
||||
redis_key = OAUTH_REFRESH_TOKEN_REDIS_KEY.format(client_id=client_id, token=token)
|
||||
redis_client.set(redis_key, user_account_id, ex=OAUTH_REFRESH_TOKEN_EXPIRES_IN)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
def validate_oauth_access_token(client_id: str, token: str) -> Account | None:
|
||||
redis_key = OAUTH_ACCESS_TOKEN_REDIS_KEY.format(client_id=client_id, token=token)
|
||||
user_account_id = redis_client.get(redis_key)
|
||||
if not user_account_id:
|
||||
return None
|
||||
|
||||
user_id_str = user_account_id.decode("utf-8")
|
||||
|
||||
return AccountService.load_user(user_id_str)
|
||||
@@ -1,6 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
import click
|
||||
from celery import shared_task # type: ignore
|
||||
@@ -14,7 +13,7 @@ from models.dataset import Document as DatasetDocument
|
||||
|
||||
|
||||
@shared_task(queue="dataset")
|
||||
def deal_dataset_vector_index_task(dataset_id: str, action: Literal["remove", "add", "update"]):
|
||||
def deal_dataset_vector_index_task(dataset_id: str, action: str):
|
||||
"""
|
||||
Async deal dataset from index
|
||||
:param dataset_id: dataset_id
|
||||
|
||||
@@ -471,7 +471,7 @@ class TestAnnotationService:
|
||||
# Verify annotation was deleted
|
||||
from extensions.ext_database import db
|
||||
|
||||
deleted_annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
|
||||
deleted_annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
|
||||
assert deleted_annotation is None
|
||||
|
||||
# Verify delete_annotation_index_task was called (when annotation setting exists)
|
||||
@@ -1175,7 +1175,7 @@ class TestAnnotationService:
|
||||
AppAnnotationService.delete_app_annotation(app.id, annotation_id)
|
||||
|
||||
# Verify annotation was deleted
|
||||
deleted_annotation = db.session.query(MessageAnnotation).where(MessageAnnotation.id == annotation_id).first()
|
||||
deleted_annotation = db.session.query(MessageAnnotation).filter(MessageAnnotation.id == annotation_id).first()
|
||||
assert deleted_annotation is None
|
||||
|
||||
# Verify delete_annotation_index_task was called
|
||||
|
||||
@@ -234,7 +234,7 @@ class TestAPIBasedExtensionService:
|
||||
# Verify extension was deleted
|
||||
from extensions.ext_database import db
|
||||
|
||||
deleted_extension = db.session.query(APIBasedExtension).where(APIBasedExtension.id == extension_id).first()
|
||||
deleted_extension = db.session.query(APIBasedExtension).filter(APIBasedExtension.id == extension_id).first()
|
||||
assert deleted_extension is None
|
||||
|
||||
def test_save_extension_duplicate_name(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -484,7 +484,7 @@ class TestMessageService:
|
||||
# Verify feedback was deleted
|
||||
from extensions.ext_database import db
|
||||
|
||||
deleted_feedback = db.session.query(MessageFeedback).where(MessageFeedback.id == feedback.id).first()
|
||||
deleted_feedback = db.session.query(MessageFeedback).filter(MessageFeedback.id == feedback.id).first()
|
||||
assert deleted_feedback is None
|
||||
|
||||
def test_create_feedback_no_rating_when_not_exists(
|
||||
|
||||
@@ -469,6 +469,6 @@ class TestModelLoadBalancingService:
|
||||
|
||||
# Verify inherit config was created in database
|
||||
inherit_configs = (
|
||||
db.session.query(LoadBalancingModelConfig).where(LoadBalancingModelConfig.name == "__inherit__").all()
|
||||
db.session.query(LoadBalancingModelConfig).filter(LoadBalancingModelConfig.name == "__inherit__").all()
|
||||
)
|
||||
assert len(inherit_configs) == 1
|
||||
|
||||
@@ -887,14 +887,6 @@ API_WORKFLOW_RUN_REPOSITORY=repositories.sqlalchemy_api_workflow_run_repository.
|
||||
# API workflow node execution repository implementation
|
||||
API_WORKFLOW_NODE_EXECUTION_REPOSITORY=repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository
|
||||
|
||||
# Workflow log cleanup configuration
|
||||
# Enable automatic cleanup of workflow run logs to manage database size
|
||||
WORKFLOW_LOG_CLEANUP_ENABLED=false
|
||||
# Number of days to retain workflow run logs (default: 30 days)
|
||||
WORKFLOW_LOG_RETENTION_DAYS=30
|
||||
# Batch size for workflow log cleanup operations (default: 100)
|
||||
WORKFLOW_LOG_CLEANUP_BATCH_SIZE=100
|
||||
|
||||
# HTTP request node in workflow configuration
|
||||
HTTP_REQUEST_NODE_MAX_BINARY_SIZE=10485760
|
||||
HTTP_REQUEST_NODE_MAX_TEXT_SIZE=1048576
|
||||
|
||||
@@ -396,9 +396,6 @@ x-shared-env: &shared-api-worker-env
|
||||
CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY:-core.repositories.sqlalchemy_workflow_node_execution_repository.SQLAlchemyWorkflowNodeExecutionRepository}
|
||||
API_WORKFLOW_RUN_REPOSITORY: ${API_WORKFLOW_RUN_REPOSITORY:-repositories.sqlalchemy_api_workflow_run_repository.DifyAPISQLAlchemyWorkflowRunRepository}
|
||||
API_WORKFLOW_NODE_EXECUTION_REPOSITORY: ${API_WORKFLOW_NODE_EXECUTION_REPOSITORY:-repositories.sqlalchemy_api_workflow_node_execution_repository.DifyAPISQLAlchemyWorkflowNodeExecutionRepository}
|
||||
WORKFLOW_LOG_CLEANUP_ENABLED: ${WORKFLOW_LOG_CLEANUP_ENABLED:-false}
|
||||
WORKFLOW_LOG_RETENTION_DAYS: ${WORKFLOW_LOG_RETENTION_DAYS:-30}
|
||||
WORKFLOW_LOG_CLEANUP_BATCH_SIZE: ${WORKFLOW_LOG_CLEANUP_BATCH_SIZE:-100}
|
||||
HTTP_REQUEST_NODE_MAX_BINARY_SIZE: ${HTTP_REQUEST_NODE_MAX_BINARY_SIZE:-10485760}
|
||||
HTTP_REQUEST_NODE_MAX_TEXT_SIZE: ${HTTP_REQUEST_NODE_MAX_TEXT_SIZE:-1048576}
|
||||
HTTP_REQUEST_NODE_SSL_VERIFY: ${HTTP_REQUEST_NODE_SSL_VERIFY:-True}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Area } from 'react-easy-crop'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { RiDeleteBin5Line, RiPencilLine } from '@remixicon/react'
|
||||
import { RiPencilLine } from '@remixicon/react'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import ImageInput, { type OnImageInput } from '@/app/components/base/app-icon-picker/ImageInput'
|
||||
@@ -27,8 +27,6 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
|
||||
const [isShowAvatarPicker, setIsShowAvatarPicker] = useState(false)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [isShowDeleteConfirm, setIsShowDeleteConfirm] = useState(false)
|
||||
const [hoverArea, setHoverArea] = useState<string>('left')
|
||||
|
||||
const handleImageInput: OnImageInput = useCallback(async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
|
||||
setInputImageInfo(
|
||||
@@ -50,18 +48,6 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
}
|
||||
}, [notify, onSave, t])
|
||||
|
||||
const handleDeleteAvatar = useCallback(async () => {
|
||||
try {
|
||||
await updateUserProfile({ url: 'account/avatar', body: { avatar: '' } })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
setIsShowDeleteConfirm(false)
|
||||
onSave?.()
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: (e as Error).message })
|
||||
}
|
||||
}, [notify, onSave, t])
|
||||
|
||||
const { handleLocalFileUpload } = useLocalFileUploader({
|
||||
limit: 3,
|
||||
disabled: false,
|
||||
@@ -100,21 +86,12 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
<div className="group relative">
|
||||
<Avatar {...props} />
|
||||
<div
|
||||
onClick={() => { setIsShowAvatarPicker(true) }}
|
||||
className="absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
onClick={() => hoverArea === 'right' ? setIsShowDeleteConfirm(true) : setIsShowAvatarPicker(true)}
|
||||
onMouseMove={(e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const isRight = x > rect.width / 2
|
||||
setHoverArea(isRight ? 'right' : 'left')
|
||||
}}
|
||||
>
|
||||
{hoverArea === 'right' ? <span className="text-xs text-white">
|
||||
<RiDeleteBin5Line />
|
||||
</span> : <span className="text-xs text-white">
|
||||
<span className="text-xs text-white">
|
||||
<RiPencilLine />
|
||||
</span>}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,26 +115,6 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
closable
|
||||
className="!w-[362px] !p-6"
|
||||
isShow={isShowDeleteConfirm}
|
||||
onClose={() => setIsShowDeleteConfirm(false)}
|
||||
>
|
||||
<div className="title-2xl-semi-bold mb-3 text-text-primary">{t('common.avatar.deleteTitle')}</div>
|
||||
<p className="mb-8 text-text-secondary">{t('common.avatar.deleteDescription')}</p>
|
||||
|
||||
<div className="flex w-full items-center justify-center gap-2">
|
||||
<Button className="w-full" onClick={() => setIsShowDeleteConfirm(false)}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
|
||||
<Button variant="warning" className="w-full" onClick={handleDeleteAvatar}>
|
||||
{t('common.operation.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,11 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightUpLine, RiRobot2Line } from '@remixicon/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Button from '../components/base/button'
|
||||
import Avatar from './avatar'
|
||||
import DifyLogo from '@/app/components/base/logo/dify-logo'
|
||||
import { useCallback } from 'react'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import Avatar from './avatar'
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
import Header from '@/app/signin/_header'
|
||||
|
||||
import cn from '@/utils/classnames'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
|
||||
export default function SignInLayout({ children }: any) {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
useDocumentTitle('')
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
useEffect(() => {
|
||||
setIsLoggedIn(!!localStorage.getItem('console_token'))
|
||||
}, [])
|
||||
return <>
|
||||
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
|
||||
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
|
||||
<Header />
|
||||
<div className={cn('flex w-full grow flex-col items-center justify-center px-6 md:px-[108px]')}>
|
||||
<div className='flex flex-col md:w-[400px]'>
|
||||
{
|
||||
isLoggedIn
|
||||
? <AppContextProvider>
|
||||
{children}
|
||||
</AppContextProvider>
|
||||
: children
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{systemFeatures.branding.enabled === false && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
|
||||
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useAuthorizeOAuthApp, useOAuthAppInfo } from '@/service/use-oauth-provider'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import {
|
||||
RiAccountCircleLine,
|
||||
RiGlobalLine,
|
||||
RiInfoCardLine,
|
||||
RiMailLine,
|
||||
RiTranslate2,
|
||||
} from '@remixicon/react'
|
||||
|
||||
const SCOPE_ICON_MAP: Record<string, { icon: React.ComponentType<{ className?: string }>, label: string }> = {
|
||||
'read:name': {
|
||||
icon: RiInfoCardLine,
|
||||
label: 'Name',
|
||||
},
|
||||
'read:email': {
|
||||
icon: RiMailLine,
|
||||
label: 'Email',
|
||||
},
|
||||
'read:avatar': {
|
||||
icon: RiAccountCircleLine,
|
||||
label: 'Avatar',
|
||||
},
|
||||
'read:interface_language': {
|
||||
icon: RiTranslate2,
|
||||
label: 'Language Preference',
|
||||
},
|
||||
'read:timezone': {
|
||||
icon: RiGlobalLine,
|
||||
label: 'Timezone',
|
||||
},
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'oauth_authorize_pending'
|
||||
|
||||
function buildReturnUrl(pathname: string, search: string) {
|
||||
try {
|
||||
const base = `${globalThis.location.origin}${pathname}${search}`
|
||||
return base
|
||||
}
|
||||
catch {
|
||||
return pathname + search
|
||||
}
|
||||
}
|
||||
|
||||
export default function OAuthAuthorizePage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const client_id = searchParams.get('client_id') || ''
|
||||
const redirect_uri = searchParams.get('redirect_uri') || ''
|
||||
const response_type = searchParams.get('response_type') || 'code'
|
||||
|
||||
const { userProfile } = useAppContext()
|
||||
const { data: authAppInfo, isLoading, isError, error } = useOAuthAppInfo(client_id, redirect_uri, true)
|
||||
const { mutateAsync: authorize, isPending: authorizing } = useAuthorizeOAuthApp()
|
||||
|
||||
const isLoggedIn = useMemo(() => {
|
||||
try {
|
||||
return Boolean(localStorage.getItem('console_token'))
|
||||
}
|
||||
catch { return false }
|
||||
}, [])
|
||||
|
||||
const invalidParams = !client_id || !redirect_uri || response_type !== 'code'
|
||||
|
||||
const onLoginClick = () => {
|
||||
try {
|
||||
const returnUrl = buildReturnUrl('/account/oauth/authorize', `?client_id=${encodeURIComponent(client_id)}&redirect_uri=${encodeURIComponent(redirect_uri)}`)
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ client_id, redirect_uri, returnUrl }))
|
||||
router.push(`/signin?redirect_url=${encodeURIComponent(returnUrl)}`)
|
||||
}
|
||||
catch {
|
||||
router.push('/signin')
|
||||
}
|
||||
}
|
||||
|
||||
const onAuthorize = async () => {
|
||||
if (!client_id || !redirect_uri)
|
||||
return
|
||||
try {
|
||||
const { code } = await authorize({ client_id })
|
||||
const url = new URL(redirect_uri)
|
||||
url.searchParams.set('code', code)
|
||||
globalThis.location.href = url.toString()
|
||||
}
|
||||
catch {
|
||||
// handled by global toast
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className='bg-background-default-subtle'>
|
||||
<Loading type='app' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (invalidParams || isError) {
|
||||
return (
|
||||
<div className={cn('mx-auto mt-8 w-full px-6 md:px-[108px]')}>
|
||||
<p className='body-md-regular mt-2 text-text-tertiary'>{(error as any)?.message || 'Invalid parameters'}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-background-default-subtle'>
|
||||
{authAppInfo?.app_icon && (
|
||||
<div className='w-max rounded-2xl border-[0.5px] border-components-panel-border bg-text-primary-on-surface p-3 shadow-lg'>
|
||||
{/* <img src={authAppInfo.app_icon} alt='app icon' className='h-10 w-10 rounded' /> */}
|
||||
<img src={'https://cloud.dify.ai/console/api/workspaces/current/tool-provider/builtin/time/icon'} alt='app icon' className='h-10 w-10 rounded' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`mb-4 mt-5 flex flex-col gap-2 ${isLoggedIn ? 'pb-2' : ''}`}>
|
||||
<div className='title-4xl-semi-bold'>
|
||||
{isLoggedIn && <div className='text-text-primary'>Connect to</div>}
|
||||
<div className='text-[var(--color-saas-dify-blue-inverted)]'>{authAppInfo?.app_label?.en_US || authAppInfo?.app_label?.zh_Hans || authAppInfo?.app_label?.ja_JP}</div>
|
||||
{!isLoggedIn && <div className='text-text-primary'>wants to access your Dify Cloud account</div>}
|
||||
</div>
|
||||
<div className='body-md-regular text-text-secondary'>{isLoggedIn ? `${authAppInfo?.app_label?.en_US} wants to access your Dify account` : 'Please log in to authorize'}</div>
|
||||
</div>
|
||||
|
||||
{isLoggedIn && (
|
||||
<div className='flex items-center justify-between rounded-xl bg-background-section-burn p-3'>
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
<div>
|
||||
<div className='system-md-semi-bold text-text-secondary'>{userProfile.name}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{userProfile.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant='tertiary' size='small' onClick={() => router.push('/signin')}>Switch account</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoggedIn && Boolean(authAppInfo?.scope) && (
|
||||
<div className='mt-2 flex flex-col gap-2.5 rounded-xl bg-background-section-burn px-[22px] py-5 text-text-secondary'>
|
||||
{authAppInfo!.scope.split(/\s+/).filter(Boolean).map((scope: string) => {
|
||||
const Icon = SCOPE_ICON_MAP[scope]
|
||||
return (
|
||||
<div key={scope} className='body-sm-medium flex items-center gap-2 text-text-secondary'>
|
||||
{Icon ? <Icon.icon className='h-4 w-4' /> : <RiAccountCircleLine className='h-4 w-4' />}
|
||||
{Icon.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='flex flex-col items-center gap-2 pt-4'>
|
||||
{!isLoggedIn ? (
|
||||
<Button variant='primary' size='large' className='w-full' onClick={onLoginClick}>Login</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button variant='primary' size='large' className='w-full' onClick={onAuthorize} disabled={authorizing} loading={authorizing}>Authorize</Button>
|
||||
<Button size='large' className='w-full' onClick={() => router.push('/apps')}>Cancel</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-4 py-2'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="1" viewBox="0 0 400 1" fill="none">
|
||||
<path d="M0 0.5H400" stroke="url(#paint0_linear_2_5904)" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2_5904" x1="400" y1="9.49584" x2="0.000228929" y2="9.17666" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.01" />
|
||||
<stop offset="0.505" stop-color="#101828" stop-opacity="0.08" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0.01" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-3 text-text-tertiary'>We respect your privacy and will only use this information to enhance your experience with our developer tools.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
ideal_output: ideaOutput,
|
||||
language: languageMap[codeLanguages] || 'javascript',
|
||||
})
|
||||
if((res as any).code) // not current or current is the same as the template would return a code field
|
||||
if(!currentCode)
|
||||
res.modified = (res as any).code
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -30,7 +30,7 @@ const BaseField = ({
|
||||
inputClassName,
|
||||
formSchema,
|
||||
field,
|
||||
disabled,
|
||||
disabled: propsDisabled,
|
||||
}: BaseFieldProps) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const {
|
||||
@@ -40,7 +40,9 @@ const BaseField = ({
|
||||
options,
|
||||
labelClassName: formLabelClassName,
|
||||
show_on = [],
|
||||
disabled: formSchemaDisabled,
|
||||
} = formSchema
|
||||
const disabled = propsDisabled || formSchemaDisabled
|
||||
|
||||
const memorizedLabel = useMemo(() => {
|
||||
if (isValidElement(label))
|
||||
@@ -85,7 +87,7 @@ const BaseField = ({
|
||||
value: option.value,
|
||||
}
|
||||
}) || []
|
||||
}, [options, renderI18nObject])
|
||||
}, [options, renderI18nObject, optionValues])
|
||||
const value = useStore(field.form.store, s => s.values[field.name])
|
||||
const values = useStore(field.form.store, (s) => {
|
||||
return show_on.reduce((acc, condition) => {
|
||||
@@ -182,9 +184,10 @@ const BaseField = ({
|
||||
className={cn(
|
||||
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
|
||||
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
inputClassName,
|
||||
)}
|
||||
onClick={() => field.handleChange(option.value)}
|
||||
onClick={() => !disabled && field.handleChange(option.value)}
|
||||
>
|
||||
{
|
||||
formSchema.showRadioUI && (
|
||||
|
||||
@@ -1,34 +1,52 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
isValidElement,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { FormSchema } from '../types'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
|
||||
export const useGetValidators = () => {
|
||||
const { t } = useTranslation()
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const getLabel = useCallback((label: string | Record<string, string> | ReactNode) => {
|
||||
if (isValidElement(label))
|
||||
return ''
|
||||
|
||||
if (typeof label === 'string')
|
||||
return label
|
||||
|
||||
if (typeof label === 'object' && label !== null)
|
||||
return renderI18nObject(label as Record<string, string>)
|
||||
}, [])
|
||||
const getValidators = useCallback((formSchema: FormSchema) => {
|
||||
const {
|
||||
name,
|
||||
validators,
|
||||
required,
|
||||
label,
|
||||
} = formSchema
|
||||
let mergedValidators = validators
|
||||
const memorizedLabel = getLabel(label)
|
||||
if (required && !validators) {
|
||||
mergedValidators = {
|
||||
onMount: ({ value }: any) => {
|
||||
if (!value)
|
||||
return t('common.errorMsg.fieldRequired', { field: name })
|
||||
return t('common.errorMsg.fieldRequired', { field: memorizedLabel || name })
|
||||
},
|
||||
onChange: ({ value }: any) => {
|
||||
if (!value)
|
||||
return t('common.errorMsg.fieldRequired', { field: name })
|
||||
return t('common.errorMsg.fieldRequired', { field: memorizedLabel || name })
|
||||
},
|
||||
onBlur: ({ value }: any) => {
|
||||
if (!value)
|
||||
return t('common.errorMsg.fieldRequired', { field: name })
|
||||
return t('common.errorMsg.fieldRequired', { field: memorizedLabel })
|
||||
},
|
||||
}
|
||||
}
|
||||
return mergedValidators
|
||||
}, [t])
|
||||
}, [t, getLabel])
|
||||
|
||||
return {
|
||||
getValidators,
|
||||
|
||||
@@ -59,6 +59,7 @@ export type FormSchema = {
|
||||
labelClassName?: string
|
||||
validators?: AnyValidators
|
||||
showRadioUI?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type FormValues = Record<string, any>
|
||||
|
||||
@@ -181,6 +181,27 @@ export type QuotaConfiguration = {
|
||||
is_valid: boolean
|
||||
}
|
||||
|
||||
export type Credential = {
|
||||
credential_id: string
|
||||
credential_name?: string
|
||||
}
|
||||
|
||||
export type CustomModel = {
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
}
|
||||
|
||||
export type CustomModelCredential = CustomModel & {
|
||||
credentials?: Record<string, any>
|
||||
available_model_credentials?: Credential[]
|
||||
current_credential_id?: string
|
||||
}
|
||||
|
||||
export type CredentialWithModel = Credential & {
|
||||
model: string
|
||||
model_type: ModelTypeEnum
|
||||
}
|
||||
|
||||
export type ModelProvider = {
|
||||
provider: string
|
||||
label: TypeWithI18N
|
||||
@@ -207,6 +228,10 @@ export type ModelProvider = {
|
||||
preferred_provider_type: PreferredProviderTypeEnum
|
||||
custom_configuration: {
|
||||
status: CustomConfigurationStatusEnum
|
||||
current_credential_id?: string
|
||||
current_credential_name?: string
|
||||
available_credentials?: Credential[]
|
||||
custom_models?: CustomModelCredential[]
|
||||
}
|
||||
system_configuration: {
|
||||
enabled: boolean
|
||||
@@ -272,9 +297,22 @@ export type ModelLoadBalancingConfigEntry = {
|
||||
in_cooldown?: boolean
|
||||
/** cooldown time (in seconds) */
|
||||
ttl?: number
|
||||
credential_id?: string
|
||||
}
|
||||
|
||||
export type ModelLoadBalancingConfig = {
|
||||
enabled: boolean
|
||||
configs: ModelLoadBalancingConfigEntry[]
|
||||
}
|
||||
|
||||
export type ProviderCredential = {
|
||||
credentials: Record<string, any>
|
||||
name: string
|
||||
credential_id: string
|
||||
}
|
||||
|
||||
export type ModelCredential = {
|
||||
credentials: Record<string, any>
|
||||
load_balancing: ModelLoadBalancingConfig
|
||||
available_credentials: Credential[]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
DefaultModel,
|
||||
DefaultModelResponse,
|
||||
Model,
|
||||
@@ -77,16 +79,17 @@ export const useProviderCredentialsAndLoadBalancing = (
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
configured?: boolean,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
credentialId?: string,
|
||||
) => {
|
||||
const { data: predefinedFormSchemasValue, mutate: mutatePredefined } = useSWR(
|
||||
(configurationMethod === ConfigurationMethodEnum.predefinedModel && configured)
|
||||
? `/workspaces/current/model-providers/${provider}/credentials`
|
||||
const { data: predefinedFormSchemasValue, mutate: mutatePredefined, isLoading: isPredefinedLoading } = useSWR(
|
||||
(configurationMethod === ConfigurationMethodEnum.predefinedModel && configured && credentialId)
|
||||
? `/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`
|
||||
: null,
|
||||
fetchModelProviderCredentials,
|
||||
)
|
||||
const { data: customFormSchemasValue, mutate: mutateCustomized } = useSWR(
|
||||
(configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields)
|
||||
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}`
|
||||
const { data: customFormSchemasValue, mutate: mutateCustomized, isLoading: isCustomizedLoading } = useSWR(
|
||||
(configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields && credentialId)
|
||||
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}${credentialId ? `&credential_id=${credentialId}` : ''}`
|
||||
: null,
|
||||
fetchModelProviderCredentials,
|
||||
)
|
||||
@@ -102,6 +105,7 @@ export const useProviderCredentialsAndLoadBalancing = (
|
||||
: undefined
|
||||
}, [
|
||||
configurationMethod,
|
||||
credentialId,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
customFormSchemasValue?.credentials,
|
||||
predefinedFormSchemasValue?.credentials,
|
||||
@@ -119,6 +123,7 @@ export const useProviderCredentialsAndLoadBalancing = (
|
||||
: customFormSchemasValue
|
||||
)?.load_balancing,
|
||||
mutate,
|
||||
isLoading: isPredefinedLoading || isCustomizedLoading,
|
||||
}
|
||||
// as ([Record<string, string | boolean | undefined> | undefined, ModelLoadBalancingConfig | undefined])
|
||||
}
|
||||
@@ -313,40 +318,57 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelModalHandler = () => {
|
||||
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
|
||||
export const useRefreshModel = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const updateModelProviders = useUpdateModelProviders()
|
||||
const updateModelList = useUpdateModelList()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const handleRefreshModel = useCallback((provider: ModelProvider, configurationMethod: ConfigurationMethodEnum, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => {
|
||||
updateModelProviders()
|
||||
|
||||
provider.supported_model_types.forEach((type) => {
|
||||
updateModelList(type)
|
||||
})
|
||||
|
||||
if (configurationMethod === ConfigurationMethodEnum.customizableModel
|
||||
&& provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
|
||||
payload: provider.provider,
|
||||
} as any)
|
||||
|
||||
if (CustomConfigurationModelFixedFields?.__model_type)
|
||||
updateModelList(CustomConfigurationModelFixedFields.__model_type)
|
||||
}
|
||||
}, [eventEmitter, updateModelList, updateModelProviders])
|
||||
|
||||
return {
|
||||
handleRefreshModel,
|
||||
}
|
||||
}
|
||||
|
||||
export const useModelModalHandler = () => {
|
||||
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
|
||||
const { handleRefreshModel } = useRefreshModel()
|
||||
|
||||
return (
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
isModelCredential?: boolean,
|
||||
credential?: Credential,
|
||||
model?: CustomModel,
|
||||
) => {
|
||||
setShowModelModal({
|
||||
payload: {
|
||||
currentProvider: provider,
|
||||
currentConfigurationMethod: configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
|
||||
isModelCredential,
|
||||
credential,
|
||||
model,
|
||||
},
|
||||
onSaveCallback: () => {
|
||||
updateModelProviders()
|
||||
|
||||
provider.supported_model_types.forEach((type) => {
|
||||
updateModelList(type)
|
||||
})
|
||||
|
||||
if (configurationMethod === ConfigurationMethodEnum.customizableModel
|
||||
&& provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
|
||||
eventEmitter?.emit({
|
||||
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
|
||||
payload: provider.provider,
|
||||
} as any)
|
||||
|
||||
if (CustomConfigurationModelFixedFields?.__model_type)
|
||||
updateModelList(CustomConfigurationModelFixedFields.__model_type)
|
||||
}
|
||||
handleRefreshModel(provider, configurationMethod, CustomConfigurationModelFixedFields)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
import SystemModelSelector from './system-model-selector'
|
||||
import ProviderAddedCard from './provider-added-card'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from './declarations'
|
||||
import {
|
||||
@@ -18,7 +16,6 @@ import {
|
||||
} from './declarations'
|
||||
import {
|
||||
useDefaultModel,
|
||||
useModelModalHandler,
|
||||
} from './hooks'
|
||||
import InstallFromMarketplace from './install-from-marketplace'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@@ -84,8 +81,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
return [filteredConfiguredProviders, filteredNotConfiguredProviders]
|
||||
}, [configuredProviders, debouncedSearchText, notConfiguredProviders])
|
||||
|
||||
const handleOpenModal = useModelModalHandler()
|
||||
|
||||
return (
|
||||
<div className='relative -mt-2 pt-1'>
|
||||
<div className={cn('mb-2 flex items-center')}>
|
||||
@@ -126,7 +121,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
<ProviderAddedCard
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -140,7 +134,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
notConfigured
|
||||
key={provider.provider}
|
||||
provider={provider}
|
||||
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Authorized } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import cn from '@/utils/classnames'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModelCredential,
|
||||
ModelCredential,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
type AddCredentialInLoadBalancingProps = {
|
||||
provider: ModelProvider
|
||||
model: CustomModelCredential
|
||||
configurationMethod: ConfigurationMethodEnum
|
||||
modelCredential: ModelCredential
|
||||
onSelectCredential: (credential: Credential) => void
|
||||
onUpdate?: () => void
|
||||
}
|
||||
const AddCredentialInLoadBalancing = ({
|
||||
provider,
|
||||
model,
|
||||
configurationMethod,
|
||||
modelCredential,
|
||||
onSelectCredential,
|
||||
onUpdate,
|
||||
}: AddCredentialInLoadBalancingProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
available_credentials,
|
||||
} = modelCredential
|
||||
const customModel = configurationMethod === ConfigurationMethodEnum.customizableModel
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'system-sm-medium flex h-8 items-center rounded-lg px-3 text-text-accent hover:bg-state-base-hover',
|
||||
open && 'bg-state-base-hover',
|
||||
)}>
|
||||
<RiAddLine className='mr-2 h-4 w-4' />
|
||||
{
|
||||
customModel
|
||||
? t('common.modelProvider.auth.addCredential')
|
||||
: t('common.modelProvider.auth.addApiKey')
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
renderTrigger={renderTrigger}
|
||||
items={[
|
||||
{
|
||||
title: customModel ? t('common.modelProvider.auth.modelCredentials') : t('common.modelProvider.auth.apiKeys'),
|
||||
model,
|
||||
credentials: available_credentials ?? [],
|
||||
},
|
||||
]}
|
||||
configurationMethod={configurationMethod}
|
||||
onItemClick={onSelectCredential}
|
||||
placement='bottom-start'
|
||||
onUpdate={onUpdate}
|
||||
isModelCredential={customModel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddCredentialInLoadBalancing)
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAddCircleFill,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
Button,
|
||||
} from '@/app/components/base/button'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Authorized from './authorized'
|
||||
import {
|
||||
useAuth,
|
||||
useCustomModels,
|
||||
} from './hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type AddCustomModelProps = {
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
}
|
||||
const AddCustomModel = ({
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
}: AddCustomModelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const customModels = useCustomModels(provider)
|
||||
const noModels = !customModels.length
|
||||
const {
|
||||
handleOpenModal,
|
||||
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, true)
|
||||
const handleClick = useCallback(() => {
|
||||
handleOpenModal()
|
||||
}, [handleOpenModal])
|
||||
const ButtonComponent = useMemo(() => {
|
||||
return (
|
||||
<Button
|
||||
variant='ghost-accent'
|
||||
size='small'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.modelProvider.addModel')}
|
||||
</Button>
|
||||
)
|
||||
}, [handleClick])
|
||||
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
return (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
className={cn(open && 'bg-components-button-ghost-bg-hover')}
|
||||
>
|
||||
<RiAddCircleFill className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.modelProvider.addModel')}
|
||||
</Button>
|
||||
)
|
||||
}, [t])
|
||||
|
||||
if (noModels)
|
||||
return ButtonComponent
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
items={customModels.map(model => ({
|
||||
model,
|
||||
credentials: model.available_model_credentials ?? [],
|
||||
}))}
|
||||
renderTrigger={renderTrigger}
|
||||
isModelCredential
|
||||
enableAddModelCredential
|
||||
bottomAddModelCredentialText={t('common.modelProvider.auth.addNewModel')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddCustomModel)
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CredentialItem from './credential-item'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModel,
|
||||
CustomModelCredential,
|
||||
} from '../../declarations'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type AuthorizedItemProps = {
|
||||
model?: CustomModelCredential
|
||||
title?: string
|
||||
disabled?: boolean
|
||||
onDelete?: (credential?: Credential, model?: CustomModel) => void
|
||||
onEdit?: (credential?: Credential, model?: CustomModel) => void
|
||||
showItemSelectedIcon?: boolean
|
||||
selectedCredentialId?: string
|
||||
credentials: Credential[]
|
||||
onItemClick?: (credential: Credential, model?: CustomModel) => void
|
||||
enableAddModelCredential?: boolean
|
||||
}
|
||||
export const AuthorizedItem = ({
|
||||
model,
|
||||
title,
|
||||
credentials,
|
||||
disabled,
|
||||
onDelete,
|
||||
onEdit,
|
||||
showItemSelectedIcon,
|
||||
selectedCredentialId,
|
||||
onItemClick,
|
||||
enableAddModelCredential,
|
||||
}: AuthorizedItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const handleEdit = useCallback((credential?: Credential) => {
|
||||
onEdit?.(credential, model)
|
||||
}, [onEdit, model])
|
||||
const handleDelete = useCallback((credential?: Credential) => {
|
||||
onDelete?.(credential, model)
|
||||
}, [onDelete, model])
|
||||
const handleItemClick = useCallback((credential: Credential) => {
|
||||
onItemClick?.(credential, model)
|
||||
}, [onItemClick, model])
|
||||
|
||||
return (
|
||||
<div className='p-1'>
|
||||
{
|
||||
model && (
|
||||
<div
|
||||
className='flex h-9 items-center'
|
||||
>
|
||||
<div className='h-5 w-5 shrink-0'></div>
|
||||
<div
|
||||
className='system-md-medium mx-1 grow truncate text-text-primary'
|
||||
title={model.model}
|
||||
>
|
||||
{title ?? model.model}
|
||||
</div>
|
||||
{
|
||||
enableAddModelCredential && (
|
||||
<Tooltip
|
||||
asChild
|
||||
popupContent={t('common.modelProvider.auth.addModelCredential')}
|
||||
>
|
||||
<Button
|
||||
className='h-6 w-6 shrink-0 rounded-full p-0'
|
||||
size='small'
|
||||
variant='secondary-accent'
|
||||
onClick={() => handleEdit?.()}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
credentials.map(credential => (
|
||||
<CredentialItem
|
||||
key={credential.credential_id}
|
||||
credential={credential}
|
||||
disabled={disabled}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
showSelectedIcon={showItemSelectedIcon}
|
||||
selectedCredentialId={selectedCredentialId}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AuthorizedItem)
|
||||
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCheckLine,
|
||||
RiDeleteBinLine,
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Credential } from '../../declarations'
|
||||
|
||||
type CredentialItemProps = {
|
||||
credential: Credential
|
||||
disabled?: boolean
|
||||
onDelete?: (credential: Credential) => void
|
||||
onEdit?: (credential?: Credential) => void
|
||||
onItemClick?: (credential: Credential) => void
|
||||
disableRename?: boolean
|
||||
disableEdit?: boolean
|
||||
disableDelete?: boolean
|
||||
showSelectedIcon?: boolean
|
||||
selectedCredentialId?: string
|
||||
}
|
||||
const CredentialItem = ({
|
||||
credential,
|
||||
disabled,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onItemClick,
|
||||
disableRename,
|
||||
disableEdit,
|
||||
disableDelete,
|
||||
showSelectedIcon,
|
||||
selectedCredentialId,
|
||||
}: CredentialItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const showAction = useMemo(() => {
|
||||
return !(disableRename && disableEdit && disableDelete)
|
||||
}, [disableRename, disableEdit, disableDelete])
|
||||
|
||||
return (
|
||||
<div
|
||||
key={credential.credential_id}
|
||||
className={cn(
|
||||
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => onItemClick?.(credential)}
|
||||
>
|
||||
<div className='flex w-0 grow items-center space-x-1.5'>
|
||||
{
|
||||
showSelectedIcon && (
|
||||
<div className='h-4 w-4'>
|
||||
{
|
||||
selectedCredentialId === credential.credential_id && (
|
||||
<RiCheckLine className='h-4 w-4 text-text-accent' />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Indicator className='ml-2 mr-1.5 shrink-0' />
|
||||
<div
|
||||
className='system-md-regular truncate text-text-secondary'
|
||||
title={credential.credential_name}
|
||||
>
|
||||
{credential.credential_name}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showAction && (
|
||||
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
|
||||
{
|
||||
!disableEdit && (
|
||||
<Tooltip popupContent={t('common.operation.edit')}>
|
||||
<ActionButton
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onEdit?.(credential)
|
||||
}}
|
||||
>
|
||||
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{
|
||||
!disableDelete && (
|
||||
<Tooltip popupContent={t('common.operation.delete')}>
|
||||
<ActionButton
|
||||
className='hover:bg-transparent'
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete?.(credential)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CredentialItem)
|
||||
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type {
|
||||
PortalToFollowElemOptions,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import { useAuth } from '../hooks'
|
||||
import AuthorizedItem from './authorized-item'
|
||||
|
||||
type AuthorizedProps = {
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
isModelCredential?: boolean
|
||||
items: {
|
||||
model?: CustomModel
|
||||
credentials: Credential[]
|
||||
}[]
|
||||
selectedCredential?: Credential
|
||||
disabled?: boolean
|
||||
renderTrigger?: (open?: boolean) => React.ReactNode
|
||||
isOpen?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
offset?: PortalToFollowElemOptions['offset']
|
||||
placement?: PortalToFollowElemOptions['placement']
|
||||
triggerPopupSameWidth?: boolean
|
||||
popupClassName?: string
|
||||
showItemSelectedIcon?: boolean
|
||||
onUpdate?: () => void
|
||||
onItemClick?: (credential: Credential, model?: CustomModel) => void
|
||||
enableAddModelCredential?: boolean
|
||||
bottomAddModelCredentialText?: string
|
||||
}
|
||||
const Authorized = ({
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
items,
|
||||
isModelCredential,
|
||||
selectedCredential,
|
||||
disabled,
|
||||
renderTrigger,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
offset = 8,
|
||||
placement = 'bottom-end',
|
||||
triggerPopupSameWidth = false,
|
||||
popupClassName,
|
||||
showItemSelectedIcon,
|
||||
onUpdate,
|
||||
onItemClick,
|
||||
enableAddModelCredential,
|
||||
bottomAddModelCredentialText,
|
||||
}: AuthorizedProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isLocalOpen, setIsLocalOpen] = useState(false)
|
||||
const mergedIsOpen = isOpen ?? isLocalOpen
|
||||
const setMergedIsOpen = useCallback((open: boolean) => {
|
||||
if (onOpenChange)
|
||||
onOpenChange(open)
|
||||
|
||||
setIsLocalOpen(open)
|
||||
}, [onOpenChange])
|
||||
const {
|
||||
openConfirmDelete,
|
||||
closeConfirmDelete,
|
||||
doingAction,
|
||||
handleActiveCredential,
|
||||
handleConfirmDelete,
|
||||
deleteCredentialId,
|
||||
handleOpenModal,
|
||||
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onUpdate)
|
||||
|
||||
const handleEdit = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
handleOpenModal(credential, model)
|
||||
setMergedIsOpen(false)
|
||||
}, [handleOpenModal, setMergedIsOpen])
|
||||
|
||||
const handleItemClick = useCallback((credential: Credential, model?: CustomModel) => {
|
||||
if (!onItemClick)
|
||||
return handleActiveCredential(credential, model)
|
||||
|
||||
onItemClick?.(credential, model)
|
||||
}, [handleActiveCredential, onItemClick])
|
||||
|
||||
return (
|
||||
<>
|
||||
<PortalToFollowElem
|
||||
open={mergedIsOpen}
|
||||
onOpenChange={setMergedIsOpen}
|
||||
placement={placement}
|
||||
offset={offset}
|
||||
triggerPopupSameWidth={triggerPopupSameWidth}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => setMergedIsOpen(!mergedIsOpen)}
|
||||
asChild
|
||||
>
|
||||
{
|
||||
renderTrigger
|
||||
? renderTrigger(mergedIsOpen)
|
||||
: (
|
||||
<Button
|
||||
className='grow'
|
||||
size='small'
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.operation.config')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[100]'>
|
||||
<div className={cn(
|
||||
'w-[360px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
|
||||
popupClassName,
|
||||
)}>
|
||||
<div className='max-h-[304px] overflow-y-auto'>
|
||||
{
|
||||
items.map((item, index) => (
|
||||
<AuthorizedItem
|
||||
key={index}
|
||||
model={item.model}
|
||||
credentials={item.credentials}
|
||||
disabled={disabled}
|
||||
onDelete={openConfirmDelete}
|
||||
onEdit={handleEdit}
|
||||
showItemSelectedIcon={showItemSelectedIcon}
|
||||
selectedCredentialId={selectedCredential?.credential_id}
|
||||
onItemClick={handleItemClick}
|
||||
enableAddModelCredential={enableAddModelCredential}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className='h-[1px] bg-divider-subtle'></div>
|
||||
{
|
||||
isModelCredential && (
|
||||
<div
|
||||
onClick={() => handleEdit()}
|
||||
className='system-xs-medium flex h-[30px] cursor-pointer items-center px-3 text-text-accent-light-mode-only'
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{bottomAddModelCredentialText ?? t('common.modelProvider.auth.addModelCredential')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isModelCredential && (
|
||||
<div className='p-2'>
|
||||
<Button
|
||||
onClick={() => handleEdit()}
|
||||
className='w-full'
|
||||
>
|
||||
{t('common.modelProvider.auth.addApiKey')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
{
|
||||
deleteCredentialId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
isDisabled={doingAction}
|
||||
onCancel={closeConfirmDelete}
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Authorized)
|
||||
@@ -0,0 +1,32 @@
|
||||
import { memo } from 'react'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ConfigModelProps = {
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
const ConfigModel = ({
|
||||
className,
|
||||
onClick,
|
||||
}: ConfigModelProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
className={cn(
|
||||
'shrink-0',
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 h-4 w-4' />
|
||||
{t('common.operation.config')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConfigModel)
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
Button,
|
||||
} from '@/app/components/base/button'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Authorized from './authorized'
|
||||
import { useAuth, useCredentialStatus } from './hooks'
|
||||
|
||||
type ConfigProviderProps = {
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
}
|
||||
const ConfigProvider = ({
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
}: ConfigProviderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
handleOpenModal,
|
||||
} = useAuth(provider, configurationMethod, currentCustomConfigurationModelFixedFields)
|
||||
const {
|
||||
hasCredential,
|
||||
authorized,
|
||||
current_credential_id,
|
||||
current_credential_name,
|
||||
available_credentials,
|
||||
} = useCredentialStatus(provider)
|
||||
const handleClick = useCallback(() => {
|
||||
if (!hasCredential)
|
||||
handleOpenModal()
|
||||
}, [handleOpenModal, hasCredential])
|
||||
|
||||
const ButtonComponent = useMemo(() => {
|
||||
return (
|
||||
<Button
|
||||
className='grow'
|
||||
size='small'
|
||||
onClick={handleClick}
|
||||
variant={!authorized ? 'secondary-accent' : 'secondary'}
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.operation.setup')}
|
||||
</Button>
|
||||
)
|
||||
}, [handleClick, authorized])
|
||||
|
||||
if (!hasCredential)
|
||||
return ButtonComponent
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
items={[
|
||||
{
|
||||
model: {
|
||||
model: t('common.modelProvider.auth.apiKeys'),
|
||||
} as any,
|
||||
credentials: available_credentials ?? [],
|
||||
},
|
||||
]}
|
||||
selectedCredential={{
|
||||
credential_id: current_credential_id ?? '',
|
||||
credential_name: current_credential_name ?? '',
|
||||
}}
|
||||
showItemSelectedIcon
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ConfigProvider)
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './use-model-form-schemas'
|
||||
export * from './use-credential-status'
|
||||
export * from './use-custom-models'
|
||||
export * from './use-auth'
|
||||
export * from './use-auth-service'
|
||||
export * from './use-credential-data'
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useActiveModelCredential,
|
||||
useActiveProviderCredential,
|
||||
useAddModelCredential,
|
||||
useAddProviderCredential,
|
||||
useDeleteModelCredential,
|
||||
useDeleteProviderCredential,
|
||||
useEditModelCredential,
|
||||
useEditProviderCredential,
|
||||
useGetModelCredential,
|
||||
useGetProviderCredential,
|
||||
} from '@/service/use-models'
|
||||
import type {
|
||||
CustomModel,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
export const useGetCredential = (provider: string, isModelCredential?: boolean, credentialId?: string, model?: CustomModel, configFrom?: string) => {
|
||||
const providerData = useGetProviderCredential(!isModelCredential && !!credentialId, provider, credentialId)
|
||||
const modelData = useGetModelCredential(!!isModelCredential && !!credentialId, provider, credentialId, model?.model, model?.model_type, configFrom)
|
||||
return isModelCredential ? modelData : providerData
|
||||
}
|
||||
|
||||
export const useAuthService = (provider: string) => {
|
||||
const { mutateAsync: addProviderCredential } = useAddProviderCredential(provider)
|
||||
const { mutateAsync: editProviderCredential } = useEditProviderCredential(provider)
|
||||
const { mutateAsync: deleteProviderCredential } = useDeleteProviderCredential(provider)
|
||||
const { mutateAsync: activeProviderCredential } = useActiveProviderCredential(provider)
|
||||
|
||||
const { mutateAsync: addModelCredential } = useAddModelCredential(provider)
|
||||
const { mutateAsync: activeModelCredential } = useActiveModelCredential(provider)
|
||||
const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider)
|
||||
const { mutateAsync: editModelCredential } = useEditModelCredential(provider)
|
||||
|
||||
const getAddCredentialService = useCallback((isModel: boolean) => {
|
||||
return isModel ? addModelCredential : addProviderCredential
|
||||
}, [addModelCredential, addProviderCredential])
|
||||
|
||||
const getEditCredentialService = useCallback((isModel: boolean) => {
|
||||
return isModel ? editModelCredential : editProviderCredential
|
||||
}, [editModelCredential, editProviderCredential])
|
||||
|
||||
const getDeleteCredentialService = useCallback((isModel: boolean) => {
|
||||
return isModel ? deleteModelCredential : deleteProviderCredential
|
||||
}, [deleteModelCredential, deleteProviderCredential])
|
||||
|
||||
const getActiveCredentialService = useCallback((isModel: boolean) => {
|
||||
return isModel ? activeModelCredential : activeProviderCredential
|
||||
}, [activeModelCredential, activeProviderCredential])
|
||||
|
||||
return {
|
||||
getAddCredentialService,
|
||||
getEditCredentialService,
|
||||
getDeleteCredentialService,
|
||||
getActiveCredentialService,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useAuthService } from './use-auth-service'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import {
|
||||
useModelModalHandler,
|
||||
useRefreshModel,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
||||
export const useAuth = (
|
||||
provider: ModelProvider,
|
||||
configurationMethod: ConfigurationMethodEnum,
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
|
||||
isModelCredential?: boolean,
|
||||
onUpdate?: () => void,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const {
|
||||
getDeleteCredentialService,
|
||||
getActiveCredentialService,
|
||||
getEditCredentialService,
|
||||
getAddCredentialService,
|
||||
} = useAuthService(provider.provider)
|
||||
const handleOpenModelModal = useModelModalHandler()
|
||||
const { handleRefreshModel } = useRefreshModel()
|
||||
const pendingOperationCredentialId = useRef<string | null>(null)
|
||||
const pendingOperationModel = useRef<CustomModel | null>(null)
|
||||
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
|
||||
const openConfirmDelete = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
if (credential)
|
||||
pendingOperationCredentialId.current = credential.credential_id
|
||||
if (model)
|
||||
pendingOperationModel.current = model
|
||||
|
||||
setDeleteCredentialId(pendingOperationCredentialId.current)
|
||||
}, [])
|
||||
const closeConfirmDelete = useCallback(() => {
|
||||
setDeleteCredentialId(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
}, [])
|
||||
const [doingAction, setDoingAction] = useState(false)
|
||||
const doingActionRef = useRef(doingAction)
|
||||
const handleSetDoingAction = useCallback((doing: boolean) => {
|
||||
doingActionRef.current = doing
|
||||
setDoingAction(doing)
|
||||
}, [])
|
||||
const handleActiveCredential = useCallback(async (credential: Credential, model?: CustomModel) => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
await getActiveCredentialService(!!model)({
|
||||
credential_id: credential.credential_id,
|
||||
model: model?.model,
|
||||
model_type: model?.model_type,
|
||||
})
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
onUpdate?.()
|
||||
handleRefreshModel(provider, configurationMethod, undefined)
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [getActiveCredentialService, onUpdate, notify, t, handleSetDoingAction])
|
||||
const handleConfirmDelete = useCallback(async () => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
if (!pendingOperationCredentialId.current) {
|
||||
setDeleteCredentialId(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
await getDeleteCredentialService(!!isModelCredential)({
|
||||
credential_id: pendingOperationCredentialId.current,
|
||||
model: pendingOperationModel.current?.model,
|
||||
model_type: pendingOperationModel.current?.model_type,
|
||||
})
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
onUpdate?.()
|
||||
handleRefreshModel(provider, configurationMethod, undefined)
|
||||
setDeleteCredentialId(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
pendingOperationModel.current = null
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [onUpdate, notify, t, handleSetDoingAction, getDeleteCredentialService, isModelCredential])
|
||||
const handleAddCredential = useCallback((model?: CustomModel) => {
|
||||
if (model)
|
||||
pendingOperationModel.current = model
|
||||
}, [])
|
||||
const handleSaveCredential = useCallback(async (payload: Record<string, any>) => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
|
||||
let res: { result?: string } = {}
|
||||
if (payload.credential_id)
|
||||
res = await getEditCredentialService(!!isModelCredential)(payload as any)
|
||||
else
|
||||
res = await getAddCredentialService(!!isModelCredential)(payload as any)
|
||||
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onUpdate?.()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [onUpdate, notify, t, handleSetDoingAction, getEditCredentialService, getAddCredentialService])
|
||||
const handleOpenModal = useCallback((credential?: Credential, model?: CustomModel) => {
|
||||
handleOpenModelModal(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
isModelCredential,
|
||||
credential,
|
||||
model,
|
||||
)
|
||||
}, [handleOpenModelModal, provider, configurationMethod, currentCustomConfigurationModelFixedFields, isModelCredential])
|
||||
|
||||
return {
|
||||
pendingOperationCredentialId,
|
||||
pendingOperationModel,
|
||||
openConfirmDelete,
|
||||
closeConfirmDelete,
|
||||
doingAction,
|
||||
handleActiveCredential,
|
||||
handleConfirmDelete,
|
||||
handleAddCredential,
|
||||
deleteCredentialId,
|
||||
handleSaveCredential,
|
||||
handleOpenModal,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useGetCredential } from './use-auth-service'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModelCredential,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
export const useCredentialData = (provider: ModelProvider, providerFormSchemaPredefined: boolean, isModelCredential?: boolean, credential?: Credential, model?: CustomModelCredential) => {
|
||||
const configFrom = useMemo(() => {
|
||||
if (providerFormSchemaPredefined)
|
||||
return 'predefined-model'
|
||||
return 'custom-model'
|
||||
}, [providerFormSchemaPredefined])
|
||||
const {
|
||||
isLoading,
|
||||
data: credentialData = {},
|
||||
} = useGetCredential(provider.provider, isModelCredential, credential?.credential_id, model, configFrom)
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
credentialData,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useMemo } from 'react'
|
||||
import type {
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
|
||||
export const useCredentialStatus = (provider: ModelProvider) => {
|
||||
const {
|
||||
current_credential_id,
|
||||
current_credential_name,
|
||||
available_credentials,
|
||||
} = provider.custom_configuration
|
||||
const hasCredential = !!available_credentials?.length
|
||||
const authorized = current_credential_id && current_credential_name
|
||||
const authRemoved = hasCredential && !current_credential_id && !current_credential_name
|
||||
|
||||
return useMemo(() => ({
|
||||
hasCredential,
|
||||
authorized,
|
||||
authRemoved,
|
||||
current_credential_id,
|
||||
current_credential_name,
|
||||
available_credentials,
|
||||
}), [hasCredential, authorized, authRemoved, current_credential_id, current_credential_name, available_credentials])
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type {
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
|
||||
export const useCustomModels = (provider: ModelProvider) => {
|
||||
const { custom_models } = provider.custom_configuration
|
||||
|
||||
return custom_models || []
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModelCredential,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelProvider,
|
||||
} from '../../declarations'
|
||||
import {
|
||||
genModelNameFormSchema,
|
||||
genModelTypeFormSchema,
|
||||
} from '../../utils'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
|
||||
export const useModelFormSchemas = (
|
||||
provider: ModelProvider,
|
||||
providerFormSchemaPredefined: boolean,
|
||||
credentials?: Record<string, any>,
|
||||
credential?: Credential,
|
||||
model?: CustomModelCredential,
|
||||
draftConfig?: ModelLoadBalancingConfig,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
provider_credential_schema,
|
||||
supported_model_types,
|
||||
model_credential_schema,
|
||||
} = provider
|
||||
const formSchemas = useMemo(() => {
|
||||
const modelTypeSchema = genModelTypeFormSchema(supported_model_types)
|
||||
const modelNameSchema = genModelNameFormSchema(model_credential_schema?.model)
|
||||
if (!!model) {
|
||||
modelTypeSchema.disabled = true
|
||||
modelNameSchema.disabled = true
|
||||
}
|
||||
return providerFormSchemaPredefined
|
||||
? provider_credential_schema.credential_form_schemas
|
||||
: [
|
||||
modelTypeSchema,
|
||||
modelNameSchema,
|
||||
...(draftConfig?.enabled ? [] : model_credential_schema.credential_form_schemas),
|
||||
]
|
||||
}, [
|
||||
providerFormSchemaPredefined,
|
||||
provider_credential_schema?.credential_form_schemas,
|
||||
supported_model_types,
|
||||
model_credential_schema?.credential_form_schemas,
|
||||
model_credential_schema?.model,
|
||||
draftConfig?.enabled,
|
||||
model,
|
||||
])
|
||||
|
||||
const formSchemasWithAuthorizationName = useMemo(() => {
|
||||
const authorizationNameSchema = {
|
||||
type: FormTypeEnum.textInput,
|
||||
variable: '__authorization_name__',
|
||||
label: t('plugin.auth.authorizationName'),
|
||||
required: true,
|
||||
}
|
||||
|
||||
return [
|
||||
authorizationNameSchema,
|
||||
...formSchemas,
|
||||
]
|
||||
}, [formSchemas, t])
|
||||
|
||||
const formValues = useMemo(() => {
|
||||
let result = {}
|
||||
if (credentials)
|
||||
result = { ...credentials }
|
||||
if (credential)
|
||||
result = { ...result, __authorization_name__: credential?.credential_name }
|
||||
if (model)
|
||||
result = { ...result, __model_name: model?.model, __model_type: model?.model_type }
|
||||
return result
|
||||
}, [credentials, credential, model])
|
||||
|
||||
return {
|
||||
formSchemas: formSchemasWithAuthorizationName,
|
||||
formValues,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { default as Authorized } from './authorized'
|
||||
export { default as SwitchCredentialInLoadBalancing } from './switch-credential-in-load-balancing'
|
||||
export { default as AddCredentialInLoadBalancing } from './add-credential-in-load-balancing'
|
||||
export { default as AddCustomModel } from './add-custom-model'
|
||||
export { default as ConfigProvider } from './config-provider'
|
||||
export { default as ConfigModel } from './config-model'
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Authorized from './authorized'
|
||||
import type {
|
||||
ModelLoadBalancingConfig,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useCredentialStatus } from './hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type SwitchCredentialInLoadBalancingProps = {
|
||||
provider: ModelProvider
|
||||
draftConfig?: ModelLoadBalancingConfig
|
||||
setDraftConfig: Dispatch<SetStateAction<ModelLoadBalancingConfig | undefined>>
|
||||
}
|
||||
const SwitchCredentialInLoadBalancing = ({
|
||||
provider,
|
||||
draftConfig,
|
||||
}: SwitchCredentialInLoadBalancingProps) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
available_credentials,
|
||||
current_credential_name,
|
||||
} = useCredentialStatus(provider)
|
||||
|
||||
const handleItemClick = useCallback(() => {
|
||||
console.log('handleItemClick', draftConfig)
|
||||
}, [])
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
const selectedCredentialId = draftConfig?.configs.find(config => config.name === '__inherit__')?.credential_id
|
||||
const selectedCredential = available_credentials?.find(credential => credential.credential_id === selectedCredentialId)
|
||||
const name = selectedCredential?.credential_name || current_credential_name
|
||||
const authRemoved = !!selectedCredentialId && !selectedCredential
|
||||
return (
|
||||
<Button
|
||||
variant='secondary'
|
||||
className={cn(
|
||||
'shrink-0 space-x-1',
|
||||
authRemoved && 'text-components-button-destructive-secondary-text',
|
||||
)}
|
||||
>
|
||||
<Indicator
|
||||
className='mr-2'
|
||||
color={authRemoved ? 'red' : 'green'}
|
||||
/>
|
||||
{
|
||||
authRemoved ? t('common.model.authRemoved') : name
|
||||
}
|
||||
{
|
||||
!authRemoved && (
|
||||
<Badge>enterprise</Badge>
|
||||
)
|
||||
}
|
||||
<RiArrowDownSLine className='h-4 w-4' />
|
||||
</Button>
|
||||
)
|
||||
}, [current_credential_name, t, draftConfig, available_credentials])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
items={[
|
||||
{
|
||||
model: {
|
||||
model: t('common.modelProvider.modelCredentials'),
|
||||
} as any,
|
||||
credentials: available_credentials || [],
|
||||
},
|
||||
]}
|
||||
renderTrigger={renderTrigger}
|
||||
onItemClick={handleItemClick}
|
||||
isModelCredential
|
||||
enableAddModelCredential
|
||||
bottomAddModelCredentialText={t('common.modelProvider.addModelCredential')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(SwitchCredentialInLoadBalancing)
|
||||
@@ -2,43 +2,20 @@ import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiErrorWarningFill,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
CredentialFormSchema,
|
||||
CredentialFormSchemaRadio,
|
||||
CredentialFormSchemaSelect,
|
||||
CustomConfigurationModelFixedFields,
|
||||
FormValue,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationStatusEnum,
|
||||
FormTypeEnum,
|
||||
} from '../declarations'
|
||||
import {
|
||||
genModelNameFormSchema,
|
||||
genModelTypeFormSchema,
|
||||
removeCredentials,
|
||||
saveCredentials,
|
||||
} from '../utils'
|
||||
import {
|
||||
useLanguage,
|
||||
useProviderCredentialsAndLoadBalancing,
|
||||
} from '../hooks'
|
||||
import { useValidate } from '../../key-validator/hooks'
|
||||
import { ValidatedStatus } from '../../key-validator/declarations'
|
||||
import ModelLoadBalancingConfigs from '../provider-added-card/model-load-balancing-configs'
|
||||
import Form from './Form'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
@@ -46,9 +23,23 @@ import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
|
||||
import type {
|
||||
FormRefObject,
|
||||
FormSchema,
|
||||
} from '@/app/components/base/form/types'
|
||||
import { useModelFormSchemas } from '../model-auth/hooks'
|
||||
import type {
|
||||
Credential,
|
||||
CustomModel,
|
||||
} from '../declarations'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import {
|
||||
useAuth,
|
||||
useCredentialData,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
|
||||
type ModelModalProps = {
|
||||
provider: ModelProvider
|
||||
@@ -56,6 +47,9 @@ type ModelModalProps = {
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
onCancel: () => void
|
||||
onSave: () => void
|
||||
model?: CustomModel
|
||||
credential?: Credential
|
||||
isModelCredential?: boolean
|
||||
}
|
||||
|
||||
const ModelModal: FC<ModelModalProps> = ({
|
||||
@@ -64,210 +58,71 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
onCancel,
|
||||
onSave,
|
||||
model,
|
||||
credential,
|
||||
isModelCredential,
|
||||
}) => {
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const {
|
||||
isLoading,
|
||||
credentialData,
|
||||
} = useCredentialData(provider, providerFormSchemaPredefined, isModelCredential, credential, model)
|
||||
const {
|
||||
handleSaveCredential,
|
||||
handleConfirmDelete,
|
||||
deleteCredentialId,
|
||||
closeConfirmDelete,
|
||||
openConfirmDelete,
|
||||
doingAction,
|
||||
} = useAuth(provider, configurateMethod, currentCustomConfigurationModelFixedFields, isModelCredential, onSave)
|
||||
const {
|
||||
credentials: formSchemasValue,
|
||||
loadBalancing: originalConfig,
|
||||
mutate,
|
||||
} = useProviderCredentialsAndLoadBalancing(
|
||||
provider.provider,
|
||||
configurateMethod,
|
||||
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
)
|
||||
} = credentialData as any
|
||||
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const language = useLanguage()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const {
|
||||
formSchemas,
|
||||
formValues,
|
||||
} = useModelFormSchemas(provider, providerFormSchemaPredefined, formSchemasValue, credential, model)
|
||||
const formRef = useRef<FormRefObject>(null)
|
||||
|
||||
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
|
||||
const originalConfigMap = useMemo(() => {
|
||||
if (!originalConfig)
|
||||
return {}
|
||||
return originalConfig?.configs.reduce((prev, config) => {
|
||||
if (config.id)
|
||||
prev[config.id] = config
|
||||
return prev
|
||||
}, {} as Record<string, ModelLoadBalancingConfigEntry>)
|
||||
}, [originalConfig])
|
||||
useEffect(() => {
|
||||
if (originalConfig && !draftConfig)
|
||||
setDraftConfig(originalConfig)
|
||||
}, [draftConfig, originalConfig])
|
||||
const handleSave = useCallback(async () => {
|
||||
const {
|
||||
isCheckValidated,
|
||||
values,
|
||||
} = formRef.current?.getFormValues({
|
||||
needCheckValidatedValues: true,
|
||||
needTransformWhenSecretFieldIsPristine: true,
|
||||
}) || { isCheckValidated: false, values: {} }
|
||||
if (!isCheckValidated)
|
||||
return
|
||||
|
||||
const formSchemas = useMemo(() => {
|
||||
return providerFormSchemaPredefined
|
||||
? provider.provider_credential_schema.credential_form_schemas
|
||||
: [
|
||||
genModelTypeFormSchema(provider.supported_model_types),
|
||||
genModelNameFormSchema(provider.model_credential_schema?.model),
|
||||
...(draftConfig?.enabled ? [] : provider.model_credential_schema.credential_form_schemas),
|
||||
]
|
||||
}, [
|
||||
providerFormSchemaPredefined,
|
||||
provider.provider_credential_schema?.credential_form_schemas,
|
||||
provider.supported_model_types,
|
||||
provider.model_credential_schema?.credential_form_schemas,
|
||||
provider.model_credential_schema?.model,
|
||||
draftConfig?.enabled,
|
||||
])
|
||||
const [
|
||||
requiredFormSchemas,
|
||||
defaultFormSchemaValue,
|
||||
showOnVariableMap,
|
||||
] = useMemo(() => {
|
||||
const requiredFormSchemas: CredentialFormSchema[] = []
|
||||
const defaultFormSchemaValue: Record<string, string | number> = {}
|
||||
const showOnVariableMap: Record<string, string[]> = {}
|
||||
|
||||
formSchemas.forEach((formSchema) => {
|
||||
if (formSchema.required)
|
||||
requiredFormSchemas.push(formSchema)
|
||||
|
||||
if (formSchema.default)
|
||||
defaultFormSchemaValue[formSchema.variable] = formSchema.default
|
||||
|
||||
if (formSchema.show_on.length) {
|
||||
formSchema.show_on.forEach((showOnItem) => {
|
||||
if (!showOnVariableMap[showOnItem.variable])
|
||||
showOnVariableMap[showOnItem.variable] = []
|
||||
|
||||
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
|
||||
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
|
||||
})
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.select || formSchema.type === FormTypeEnum.radio) {
|
||||
(formSchema as (CredentialFormSchemaRadio | CredentialFormSchemaSelect)).options.forEach((option) => {
|
||||
if (option.show_on.length) {
|
||||
option.show_on.forEach((showOnItem) => {
|
||||
if (!showOnVariableMap[showOnItem.variable])
|
||||
showOnVariableMap[showOnItem.variable] = []
|
||||
|
||||
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
|
||||
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return [
|
||||
requiredFormSchemas,
|
||||
defaultFormSchemaValue,
|
||||
showOnVariableMap,
|
||||
]
|
||||
}, [formSchemas])
|
||||
const initialFormSchemasValue: Record<string, string | number> = useMemo(() => {
|
||||
return {
|
||||
...defaultFormSchemaValue,
|
||||
...formSchemasValue,
|
||||
} as unknown as Record<string, string | number>
|
||||
}, [formSchemasValue, defaultFormSchemaValue])
|
||||
const [value, setValue] = useState(initialFormSchemasValue)
|
||||
useEffect(() => {
|
||||
setValue(initialFormSchemasValue)
|
||||
}, [initialFormSchemasValue])
|
||||
const [_, validating, validatedStatusState] = useValidate(value)
|
||||
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => {
|
||||
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return true
|
||||
|
||||
if (!requiredFormSchema.show_on.length)
|
||||
return true
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const handleValueChange = (v: FormValue) => {
|
||||
setValue(v)
|
||||
}
|
||||
|
||||
const extendedSecretFormSchemas = useMemo(
|
||||
() =>
|
||||
(providerFormSchemaPredefined
|
||||
? provider.provider_credential_schema.credential_form_schemas
|
||||
: [
|
||||
genModelTypeFormSchema(provider.supported_model_types),
|
||||
genModelNameFormSchema(provider.model_credential_schema?.model),
|
||||
...provider.model_credential_schema.credential_form_schemas,
|
||||
]).filter(({ type }) => type === FormTypeEnum.secretInput),
|
||||
[
|
||||
provider.model_credential_schema?.credential_form_schemas,
|
||||
provider.model_credential_schema?.model,
|
||||
provider.provider_credential_schema?.credential_form_schemas,
|
||||
provider.supported_model_types,
|
||||
providerFormSchemaPredefined,
|
||||
],
|
||||
)
|
||||
|
||||
const encodeSecretValues = useCallback((v: FormValue) => {
|
||||
const result = { ...v }
|
||||
extendedSecretFormSchemas.forEach(({ variable }) => {
|
||||
if (result[variable] === formSchemasValue?.[variable] && result[variable] !== undefined)
|
||||
result[variable] = '[__HIDDEN__]'
|
||||
})
|
||||
return result
|
||||
}, [extendedSecretFormSchemas, formSchemasValue])
|
||||
|
||||
const encodeConfigEntrySecretValues = useCallback((entry: ModelLoadBalancingConfigEntry) => {
|
||||
const result = { ...entry }
|
||||
extendedSecretFormSchemas.forEach(({ variable }) => {
|
||||
if (entry.id && result.credentials[variable] === originalConfigMap[entry.id]?.credentials?.[variable])
|
||||
result.credentials[variable] = '[__HIDDEN__]'
|
||||
})
|
||||
return result
|
||||
}, [extendedSecretFormSchemas, originalConfigMap])
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await saveCredentials(
|
||||
providerFormSchemaPredefined,
|
||||
provider.provider,
|
||||
encodeSecretValues(value),
|
||||
{
|
||||
...draftConfig,
|
||||
enabled: Boolean(draftConfig?.enabled),
|
||||
configs: draftConfig?.configs.map(encodeConfigEntrySecretValues) || [],
|
||||
},
|
||||
)
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
mutate()
|
||||
onSave()
|
||||
onCancel()
|
||||
}
|
||||
const {
|
||||
__authorization_name__,
|
||||
__model_name,
|
||||
__model_type,
|
||||
...rest
|
||||
} = values
|
||||
if (__model_name && __model_type) {
|
||||
handleSaveCredential({
|
||||
credential_id: credential?.credential_id,
|
||||
credentials: rest,
|
||||
name: __authorization_name__,
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
else {
|
||||
handleSaveCredential({
|
||||
credential_id: credential?.credential_id,
|
||||
credentials: rest,
|
||||
name: __authorization_name__,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const res = await removeCredentials(
|
||||
providerFormSchemaPredefined,
|
||||
provider.provider,
|
||||
value,
|
||||
)
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
mutate()
|
||||
onSave()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [handleSaveCredential, credential?.credential_id, model])
|
||||
|
||||
const renderTitlePrefix = () => {
|
||||
const prefix = isEditMode ? t('common.operation.setup') : t('common.operation.add')
|
||||
@@ -285,23 +140,29 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
</div>
|
||||
|
||||
<div className='max-h-[calc(100vh-320px)] overflow-y-auto'>
|
||||
<Form
|
||||
value={value}
|
||||
onChange={handleValueChange}
|
||||
formSchemas={formSchemas}
|
||||
validating={validating}
|
||||
validatedSuccess={validatedStatusState.status === ValidatedStatus.Success}
|
||||
showOnVariableMap={showOnVariableMap}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
<div className='mb-4 mt-1 border-t-[0.5px] border-t-divider-regular' />
|
||||
<ModelLoadBalancingConfigs withSwitch {...{
|
||||
draftConfig,
|
||||
setDraftConfig,
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
configurationMethod: configurateMethod,
|
||||
}} />
|
||||
{
|
||||
isLoading && (
|
||||
<div className='flex items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading && (
|
||||
<AuthForm
|
||||
formSchemas={formSchemas.map((formSchema) => {
|
||||
return {
|
||||
...formSchema,
|
||||
name: formSchema.variable,
|
||||
showRadioUI: formSchema.type === FormTypeEnum.radio,
|
||||
}
|
||||
}) as FormSchema[]}
|
||||
defaultValues={formValues}
|
||||
inputClassName='justify-start'
|
||||
ref={formRef}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='sticky bottom-0 -mx-2 mt-2 flex flex-wrap items-center justify-between gap-y-2 bg-components-panel-bg px-2 pb-6 pt-4'>
|
||||
@@ -327,7 +188,7 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
variant='warning'
|
||||
size='large'
|
||||
className='mr-2'
|
||||
onClick={() => setShowConfirm(true)}
|
||||
onClick={() => openConfirmDelete(credential, model)}
|
||||
>
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
@@ -344,12 +205,7 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
size='large'
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={
|
||||
loading
|
||||
|| filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)
|
||||
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
|
||||
}
|
||||
|
||||
disabled={isLoading || doingAction}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
@@ -357,38 +213,28 @@ const ModelModal: FC<ModelModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-divider-regular'>
|
||||
{
|
||||
(validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message)
|
||||
? (
|
||||
<div className='flex bg-background-section-burn px-[10px] py-3 text-xs text-[#D92D20]'>
|
||||
<RiErrorWarningFill className='mr-2 mt-[1px] h-[14px] w-[14px]' />
|
||||
{validatedStatusState.message}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showConfirm && (
|
||||
deleteCredentialId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
isShow={showConfirm}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
onConfirm={handleRemove}
|
||||
isDisabled={doingAction}
|
||||
onCancel={closeConfirmDelete}
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,348 +0,0 @@
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiErrorWarningFill,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
CredentialFormSchema,
|
||||
CredentialFormSchemaRadio,
|
||||
CredentialFormSchemaSelect,
|
||||
CredentialFormSchemaTextInput,
|
||||
CustomConfigurationModelFixedFields,
|
||||
FormValue,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
FormTypeEnum,
|
||||
} from '../declarations'
|
||||
|
||||
import {
|
||||
useLanguage,
|
||||
} from '../hooks'
|
||||
import { useValidate } from '../../key-validator/hooks'
|
||||
import { ValidatedStatus } from '../../key-validator/declarations'
|
||||
import { validateLoadBalancingCredentials } from '../utils'
|
||||
import Form from './Form'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
|
||||
type ModelModalProps = {
|
||||
provider: ModelProvider
|
||||
configurationMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
entry?: ModelLoadBalancingConfigEntry
|
||||
onCancel: () => void
|
||||
onSave: (entry: ModelLoadBalancingConfigEntry) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
const ModelLoadBalancingEntryModal: FC<ModelModalProps> = ({
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
entry,
|
||||
onCancel,
|
||||
onSave,
|
||||
onRemove,
|
||||
}) => {
|
||||
const providerFormSchemaPredefined = configurationMethod === ConfigurationMethodEnum.predefinedModel
|
||||
// const { credentials: formSchemasValue } = useProviderCredentialsAndLoadBalancing(
|
||||
// provider.provider,
|
||||
// configurationMethod,
|
||||
// providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
|
||||
// currentCustomConfigurationModelFixedFields,
|
||||
// )
|
||||
const isEditMode = !!entry
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const language = useLanguage()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const formSchemas = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
type: FormTypeEnum.textInput,
|
||||
label: {
|
||||
en_US: 'Config Name',
|
||||
zh_Hans: '配置名称',
|
||||
},
|
||||
variable: 'name',
|
||||
required: true,
|
||||
show_on: [],
|
||||
placeholder: {
|
||||
en_US: 'Enter your Config Name here',
|
||||
zh_Hans: '输入配置名称',
|
||||
},
|
||||
} as CredentialFormSchemaTextInput,
|
||||
...(
|
||||
providerFormSchemaPredefined
|
||||
? provider.provider_credential_schema.credential_form_schemas
|
||||
: provider.model_credential_schema.credential_form_schemas
|
||||
),
|
||||
]
|
||||
}, [
|
||||
providerFormSchemaPredefined,
|
||||
provider.provider_credential_schema?.credential_form_schemas,
|
||||
provider.model_credential_schema?.credential_form_schemas,
|
||||
])
|
||||
|
||||
const [
|
||||
requiredFormSchemas,
|
||||
secretFormSchemas,
|
||||
defaultFormSchemaValue,
|
||||
showOnVariableMap,
|
||||
] = useMemo(() => {
|
||||
const requiredFormSchemas: CredentialFormSchema[] = []
|
||||
const secretFormSchemas: CredentialFormSchema[] = []
|
||||
const defaultFormSchemaValue: Record<string, string | number> = {}
|
||||
const showOnVariableMap: Record<string, string[]> = {}
|
||||
|
||||
formSchemas.forEach((formSchema) => {
|
||||
if (formSchema.required)
|
||||
requiredFormSchemas.push(formSchema)
|
||||
|
||||
if (formSchema.type === FormTypeEnum.secretInput)
|
||||
secretFormSchemas.push(formSchema)
|
||||
|
||||
if (formSchema.default)
|
||||
defaultFormSchemaValue[formSchema.variable] = formSchema.default
|
||||
|
||||
if (formSchema.show_on.length) {
|
||||
formSchema.show_on.forEach((showOnItem) => {
|
||||
if (!showOnVariableMap[showOnItem.variable])
|
||||
showOnVariableMap[showOnItem.variable] = []
|
||||
|
||||
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
|
||||
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
|
||||
})
|
||||
}
|
||||
|
||||
if (formSchema.type === FormTypeEnum.select || formSchema.type === FormTypeEnum.radio) {
|
||||
(formSchema as (CredentialFormSchemaRadio | CredentialFormSchemaSelect)).options.forEach((option) => {
|
||||
if (option.show_on.length) {
|
||||
option.show_on.forEach((showOnItem) => {
|
||||
if (!showOnVariableMap[showOnItem.variable])
|
||||
showOnVariableMap[showOnItem.variable] = []
|
||||
|
||||
if (!showOnVariableMap[showOnItem.variable].includes(formSchema.variable))
|
||||
showOnVariableMap[showOnItem.variable].push(formSchema.variable)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return [
|
||||
requiredFormSchemas,
|
||||
secretFormSchemas,
|
||||
defaultFormSchemaValue,
|
||||
showOnVariableMap,
|
||||
]
|
||||
}, [formSchemas])
|
||||
const [initialValue, setInitialValue] = useState<ModelLoadBalancingConfigEntry['credentials']>()
|
||||
useEffect(() => {
|
||||
if (entry && !initialValue) {
|
||||
setInitialValue({
|
||||
...defaultFormSchemaValue,
|
||||
...entry.credentials,
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
} as Record<string, string | undefined | boolean>)
|
||||
}
|
||||
}, [entry, defaultFormSchemaValue, initialValue])
|
||||
const formSchemasValue = useMemo(() => ({
|
||||
...currentCustomConfigurationModelFixedFields,
|
||||
...initialValue,
|
||||
}), [currentCustomConfigurationModelFixedFields, initialValue])
|
||||
const initialFormSchemasValue: Record<string, string | number> = useMemo(() => {
|
||||
return {
|
||||
...defaultFormSchemaValue,
|
||||
...formSchemasValue,
|
||||
} as Record<string, string | number>
|
||||
}, [formSchemasValue, defaultFormSchemaValue])
|
||||
const [value, setValue] = useState(initialFormSchemasValue)
|
||||
useEffect(() => {
|
||||
setValue(initialFormSchemasValue)
|
||||
}, [initialFormSchemasValue])
|
||||
const [_, validating, validatedStatusState] = useValidate(value)
|
||||
const filteredRequiredFormSchemas = requiredFormSchemas.filter((requiredFormSchema) => {
|
||||
if (requiredFormSchema.show_on.length && requiredFormSchema.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value))
|
||||
return true
|
||||
|
||||
if (!requiredFormSchema.show_on.length)
|
||||
return true
|
||||
|
||||
return false
|
||||
})
|
||||
const getSecretValues = useCallback((v: FormValue) => {
|
||||
return secretFormSchemas.reduce((prev, next) => {
|
||||
if (isEditMode && v[next.variable] && v[next.variable] === initialFormSchemasValue[next.variable])
|
||||
prev[next.variable] = '[__HIDDEN__]'
|
||||
|
||||
return prev
|
||||
}, {} as Record<string, string>)
|
||||
}, [initialFormSchemasValue, isEditMode, secretFormSchemas])
|
||||
|
||||
// const handleValueChange = ({ __model_type, __model_name, ...v }: FormValue) => {
|
||||
const handleValueChange = (v: FormValue) => {
|
||||
setValue(v)
|
||||
}
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const res = await validateLoadBalancingCredentials(
|
||||
providerFormSchemaPredefined,
|
||||
provider.provider,
|
||||
{
|
||||
...value,
|
||||
...getSecretValues(value),
|
||||
},
|
||||
entry?.id,
|
||||
)
|
||||
if (res.status === ValidatedStatus.Success) {
|
||||
// notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
const { __model_type, __model_name, name, ...credentials } = value
|
||||
onSave({
|
||||
...(entry || {}),
|
||||
name: name as string,
|
||||
credentials: credentials as Record<string, string | boolean | undefined>,
|
||||
})
|
||||
// onCancel()
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: res.message || '' })
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = () => {
|
||||
onRemove?.()
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent className='z-[60] h-full w-full'>
|
||||
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
|
||||
<div className='mx-2 max-h-[calc(100vh-120px)] w-[640px] overflow-y-auto rounded-2xl bg-white shadow-xl'>
|
||||
<div className='px-8 pt-8'>
|
||||
<div className='mb-2 flex items-center justify-between'>
|
||||
<div className='text-xl font-semibold text-gray-900'>{t(isEditMode ? 'common.modelProvider.editConfig' : 'common.modelProvider.addConfig')}</div>
|
||||
</div>
|
||||
<Form
|
||||
value={value}
|
||||
onChange={handleValueChange}
|
||||
formSchemas={formSchemas}
|
||||
validating={validating}
|
||||
validatedSuccess={validatedStatusState.status === ValidatedStatus.Success}
|
||||
showOnVariableMap={showOnVariableMap}
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
<div className='sticky bottom-0 flex flex-wrap items-center justify-between gap-y-2 bg-white py-6'>
|
||||
{
|
||||
(provider.help && (provider.help.title || provider.help.url))
|
||||
? (
|
||||
<a
|
||||
href={provider.help?.url[language] || provider.help?.url.en_US}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-primary-600'
|
||||
onClick={e => !provider.help.url && e.preventDefault()}
|
||||
>
|
||||
{provider.help.title?.[language] || provider.help.url[language] || provider.help.title?.en_US || provider.help.url.en_US}
|
||||
<LinkExternal02 className='ml-1 h-3 w-3' />
|
||||
</a>
|
||||
)
|
||||
: <div />
|
||||
}
|
||||
<div>
|
||||
{
|
||||
isEditMode && (
|
||||
<Button
|
||||
size='large'
|
||||
className='mr-2 text-[#D92D20]'
|
||||
onClick={() => setShowConfirm(true)}
|
||||
>
|
||||
{t('common.operation.remove')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
size='large'
|
||||
className='mr-2'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size='large'
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
disabled={loading || filteredRequiredFormSchemas.some(item => value[item.variable] === undefined)}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-black/5'>
|
||||
{
|
||||
(validatedStatusState.status === ValidatedStatus.Error && validatedStatusState.message)
|
||||
? (
|
||||
<div className='flex bg-[#FEF3F2] px-[10px] py-3 text-xs text-[#D92D20]'>
|
||||
<RiErrorWarningFill className='mr-2 mt-[1px] h-[14px] w-[14px]' />
|
||||
{validatedStatusState.message}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='flex items-center justify-center bg-gray-50 py-3 text-xs text-gray-500'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-gray-500' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-primary-600'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showConfirm && (
|
||||
<Confirm
|
||||
title={t('common.modelProvider.confirmDelete')}
|
||||
isShow={showConfirm}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
onConfirm={handleRemove}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModelLoadBalancingEntryModal)
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { FC } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import type {
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
CustomConfigurationStatusEnum,
|
||||
@@ -15,19 +16,19 @@ import PrioritySelector from './priority-selector'
|
||||
import PriorityUseTip from './priority-use-tip'
|
||||
import { UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST } from './index'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { changeModelProviderPriority } from '@/service/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
|
||||
import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
|
||||
type CredentialPanelProps = {
|
||||
provider: ModelProvider
|
||||
onSetup: () => void
|
||||
}
|
||||
const CredentialPanel: FC<CredentialPanelProps> = ({
|
||||
const CredentialPanel = ({
|
||||
provider,
|
||||
onSetup,
|
||||
}) => {
|
||||
}: CredentialPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@@ -38,6 +39,12 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
|
||||
const priorityUseType = provider.preferred_provider_type
|
||||
const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active
|
||||
const configurateMethods = provider.configurate_methods
|
||||
const {
|
||||
hasCredential,
|
||||
authorized,
|
||||
authRemoved,
|
||||
current_credential_name,
|
||||
} = useCredentialStatus(provider)
|
||||
|
||||
const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
|
||||
const res = await changeModelProviderPriority({
|
||||
@@ -61,25 +68,42 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
|
||||
} as any)
|
||||
}
|
||||
}
|
||||
const credentialLabel = useMemo(() => {
|
||||
if (!hasCredential)
|
||||
return t('common.modelProvider.auth.unAuthorized')
|
||||
if (authorized)
|
||||
return current_credential_name
|
||||
if (authRemoved)
|
||||
return t('common.modelProvider.auth.authRemoved')
|
||||
|
||||
return ''
|
||||
}, [authorized, authRemoved, current_credential_name, hasCredential])
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
provider.provider_credential_schema && (
|
||||
<div className='relative ml-1 w-[112px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] p-1'>
|
||||
<div className='system-xs-medium-uppercase mb-1 flex h-5 items-center justify-between pl-2 pr-[7px] pt-1 text-text-tertiary'>
|
||||
API-KEY
|
||||
<Indicator color={isCustomConfigured ? 'green' : 'red'} />
|
||||
<div className={cn(
|
||||
'relative ml-1 w-[120px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] p-1',
|
||||
authRemoved && 'border-state-destructive-border bg-state-destructive-hover',
|
||||
)}>
|
||||
<div className='system-xs-medium mb-1 flex h-5 items-center justify-between pl-2 pr-[7px] pt-1 text-text-tertiary'>
|
||||
<div
|
||||
className={cn(
|
||||
'grow truncate',
|
||||
authRemoved && 'text-text-destructive',
|
||||
)}
|
||||
title={credentialLabel}
|
||||
>
|
||||
{credentialLabel}
|
||||
</div>
|
||||
<Indicator className='shrink-0' color={authorized ? 'green' : 'red'} />
|
||||
</div>
|
||||
<div className='flex items-center gap-0.5'>
|
||||
<Button
|
||||
className='grow'
|
||||
size='small'
|
||||
onClick={onSetup}
|
||||
>
|
||||
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.operation.setup')}
|
||||
</Button>
|
||||
<ConfigProvider
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.predefinedModel}
|
||||
/>
|
||||
{
|
||||
systemConfig.enabled && isCustomConfigured && (
|
||||
<PrioritySelector
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
@@ -21,23 +20,21 @@ import ModelBadge from '../model-badge'
|
||||
import CredentialPanel from './credential-panel'
|
||||
import QuotaPanel from './quota-panel'
|
||||
import ModelList from './model-list'
|
||||
import AddModelButton from './add-model-button'
|
||||
import { fetchModelProviderModelList } from '@/service/common'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import cn from '@/utils/classnames'
|
||||
import { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
|
||||
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
|
||||
type ProviderAddedCardProps = {
|
||||
notConfigured?: boolean
|
||||
provider: ModelProvider
|
||||
onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
|
||||
}
|
||||
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
notConfigured,
|
||||
provider,
|
||||
onOpenModal,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
@@ -114,7 +111,6 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
{
|
||||
showCredential && (
|
||||
<CredentialPanel
|
||||
onSetup={() => onOpenModal(ConfigurationMethodEnum.predefinedModel)}
|
||||
provider={provider}
|
||||
/>
|
||||
)
|
||||
@@ -159,9 +155,9 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
)}
|
||||
{
|
||||
configurationMethods.includes(ConfigurationMethodEnum.customizableModel) && isCurrentWorkspaceManager && (
|
||||
<AddModelButton
|
||||
onClick={() => onOpenModal(ConfigurationMethodEnum.customizableModel)}
|
||||
className='flex'
|
||||
<AddCustomModel
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -174,7 +170,6 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
|
||||
provider={provider}
|
||||
models={modelList}
|
||||
onCollapse={() => setCollapsed(true)}
|
||||
onConfig={currentCustomConfigurationModelFixedFields => onOpenModal(ConfigurationMethodEnum.customizableModel, currentCustomConfigurationModelFixedFields)}
|
||||
onChange={(provider: string) => getModelList(provider)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
import { memo, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import type { CustomConfigurationModelFixedFields, ModelItem, ModelProvider } from '../declarations'
|
||||
import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations'
|
||||
import type { ModelItem, ModelProvider } from '../declarations'
|
||||
import { ModelStatusEnum } from '../declarations'
|
||||
import ModelBadge from '../model-badge'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useProviderContext, useProviderContextSelector } from '@/context/provider-context'
|
||||
import { disableModel, enableModel } from '@/service/common'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { ConfigModel } from '../model-auth'
|
||||
|
||||
export type ModelListItemProps = {
|
||||
model: ModelItem
|
||||
provider: ModelProvider
|
||||
isConfigurable: boolean
|
||||
onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
|
||||
onModifyLoadBalancing?: (model: ModelItem) => void
|
||||
}
|
||||
|
||||
const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoadBalancing }: ModelListItemProps) => {
|
||||
const ModelListItem = ({ model, provider, isConfigurable, onModifyLoadBalancing }: ModelListItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
|
||||
@@ -46,7 +44,7 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad
|
||||
|
||||
return (
|
||||
<div
|
||||
key={model.model}
|
||||
key={`${model.model}-${model.fetch_from}`}
|
||||
className={classNames(
|
||||
'group flex h-8 items-center rounded-lg pl-2 pr-2.5',
|
||||
isConfigurable && 'hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
@@ -74,29 +72,12 @@ const ModelListItem = ({ model, provider, isConfigurable, onConfig, onModifyLoad
|
||||
</ModelName>
|
||||
<div className='flex shrink-0 items-center'>
|
||||
{
|
||||
model.fetch_from === ConfigurationMethodEnum.customizableModel
|
||||
? (isCurrentWorkspaceManager && (
|
||||
<Button
|
||||
size='small'
|
||||
className='hidden group-hover:flex'
|
||||
onClick={() => onConfig({ __model_name: model.model, __model_type: model.model_type })}
|
||||
>
|
||||
<Settings01 className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.modelProvider.config')}
|
||||
</Button>
|
||||
))
|
||||
: (isCurrentWorkspaceManager && (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status))
|
||||
? (
|
||||
<Button
|
||||
size='small'
|
||||
className='opacity-0 transition-opacity group-hover:opacity-100'
|
||||
onClick={() => onModifyLoadBalancing?.(model)}
|
||||
>
|
||||
<Balance className='mr-1 h-3.5 w-3.5' />
|
||||
{t('common.modelProvider.configLoadBalancing')}
|
||||
</Button>
|
||||
)
|
||||
: null
|
||||
(isCurrentWorkspaceManager && (modelLoadBalancingEnabled || plan.type === Plan.sandbox) && !model.deprecated && [ModelStatusEnum.active, ModelStatusEnum.disabled].includes(model.status)) && (
|
||||
<ConfigModel
|
||||
className='hidden group-hover:flex'
|
||||
onClick={() => onModifyLoadBalancing?.(model)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
model.deprecated
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
RiArrowRightSLine,
|
||||
} from '@remixicon/react'
|
||||
import type {
|
||||
CustomConfigurationModelFixedFields,
|
||||
Credential,
|
||||
ModelItem,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
@@ -13,34 +13,33 @@ import {
|
||||
ConfigurationMethodEnum,
|
||||
} from '../declarations'
|
||||
// import Tab from './tab'
|
||||
import AddModelButton from './add-model-button'
|
||||
import ModelListItem from './model-list-item'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { AddCustomModel } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
|
||||
type ModelListProps = {
|
||||
provider: ModelProvider
|
||||
models: ModelItem[]
|
||||
onCollapse: () => void
|
||||
onConfig: (currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void
|
||||
onChange?: (provider: string) => void
|
||||
}
|
||||
const ModelList: FC<ModelListProps> = ({
|
||||
provider,
|
||||
models,
|
||||
onCollapse,
|
||||
onConfig,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const configurativeMethods = provider.configurate_methods.filter(method => method !== ConfigurationMethodEnum.fetchFromRemote)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const isConfigurable = configurativeMethods.includes(ConfigurationMethodEnum.customizableModel)
|
||||
|
||||
const setShowModelLoadBalancingModal = useModalContextSelector(state => state.setShowModelLoadBalancingModal)
|
||||
const onModifyLoadBalancing = useCallback((model: ModelItem) => {
|
||||
const onModifyLoadBalancing = useCallback((model: ModelItem, credential?: Credential) => {
|
||||
setShowModelLoadBalancingModal({
|
||||
provider,
|
||||
credential,
|
||||
configurateMethod: model.fetch_from,
|
||||
model: model!,
|
||||
open: !!model,
|
||||
onClose: () => setShowModelLoadBalancingModal(null),
|
||||
@@ -65,17 +64,14 @@ const ModelList: FC<ModelListProps> = ({
|
||||
<RiArrowRightSLine className='mr-0.5 h-4 w-4 rotate-90' />
|
||||
</span>
|
||||
</span>
|
||||
{/* {
|
||||
isConfigurable && canSystemConfig && (
|
||||
<span className='flex items-center'>
|
||||
<Tab active='all' onSelect={() => {}} />
|
||||
</span>
|
||||
)
|
||||
} */}
|
||||
{
|
||||
isConfigurable && isCurrentWorkspaceManager && (
|
||||
<div className='flex grow justify-end'>
|
||||
<AddModelButton onClick={() => onConfig()} />
|
||||
<AddCustomModel
|
||||
provider={provider}
|
||||
configurationMethod={ConfigurationMethodEnum.customizableModel}
|
||||
currentCustomConfigurationModelFixedFields={undefined}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -83,12 +79,11 @@ const ModelList: FC<ModelListProps> = ({
|
||||
{
|
||||
models.map(model => (
|
||||
<ModelListItem
|
||||
key={model.model}
|
||||
key={`${model.model}-${model.fetch_from}`}
|
||||
{...{
|
||||
model,
|
||||
provider,
|
||||
isConfigurable,
|
||||
onConfig,
|
||||
onModifyLoadBalancing,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -3,22 +3,32 @@ import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEqualizer2Line,
|
||||
} from '@remixicon/react'
|
||||
import type { ConfigurationMethodEnum, CustomConfigurationModelFixedFields, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations'
|
||||
import type {
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModelCredential,
|
||||
ModelCredential,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import { ConfigurationMethodEnum } from '../declarations'
|
||||
import Indicator from '../../../indicator'
|
||||
import CooldownTimer from './cooldown-timer'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { Balance } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
|
||||
import { Edit02, Plus02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import s from '@/app/components/custom/style.module.css'
|
||||
import GridMask from '@/app/components/base/grid-mask'
|
||||
import { useProviderContextSelector } from '@/context/provider-context'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { AddCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import { useModelModalHandler } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
||||
export type ModelLoadBalancingConfigsProps = {
|
||||
draftConfig?: ModelLoadBalancingConfig
|
||||
@@ -28,19 +38,26 @@ export type ModelLoadBalancingConfigsProps = {
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
withSwitch?: boolean
|
||||
className?: string
|
||||
modelCredential: ModelCredential
|
||||
onUpdate?: () => void
|
||||
model: CustomModelCredential
|
||||
}
|
||||
|
||||
const ModelLoadBalancingConfigs = ({
|
||||
draftConfig,
|
||||
setDraftConfig,
|
||||
provider,
|
||||
model,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
withSwitch = false,
|
||||
className,
|
||||
modelCredential,
|
||||
onUpdate,
|
||||
}: ModelLoadBalancingConfigsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const modelLoadBalancingEnabled = useProviderContextSelector(state => state.modelLoadBalancingEnabled)
|
||||
const handleOpenModal = useModelModalHandler()
|
||||
|
||||
const updateConfigEntry = useCallback(
|
||||
(
|
||||
@@ -65,6 +82,21 @@ const ModelLoadBalancingConfigs = ({
|
||||
[setDraftConfig],
|
||||
)
|
||||
|
||||
const addConfigEntry = useCallback((credential: Credential) => {
|
||||
setDraftConfig((prev: any) => {
|
||||
if (!prev)
|
||||
return prev
|
||||
return {
|
||||
...prev,
|
||||
configs: [...prev.configs, {
|
||||
credential_id: credential.credential_id,
|
||||
enabled: true,
|
||||
name: credential.credential_name,
|
||||
}],
|
||||
}
|
||||
})
|
||||
}, [setDraftConfig])
|
||||
|
||||
const toggleModalBalancing = useCallback((enabled: boolean) => {
|
||||
if ((modelLoadBalancingEnabled || !enabled) && draftConfig) {
|
||||
setDraftConfig({
|
||||
@@ -81,54 +113,6 @@ const ModelLoadBalancingConfigs = ({
|
||||
}))
|
||||
}, [updateConfigEntry])
|
||||
|
||||
const setShowModelLoadBalancingEntryModal = useModalContextSelector(state => state.setShowModelLoadBalancingEntryModal)
|
||||
|
||||
const toggleEntryModal = useCallback((index?: number, entry?: ModelLoadBalancingConfigEntry) => {
|
||||
setShowModelLoadBalancingEntryModal({
|
||||
payload: {
|
||||
currentProvider: provider,
|
||||
currentConfigurationMethod: configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
entry,
|
||||
index,
|
||||
},
|
||||
onSaveCallback: ({ entry: result }) => {
|
||||
if (entry) {
|
||||
// edit
|
||||
setDraftConfig(prev => ({
|
||||
...prev,
|
||||
enabled: !!prev?.enabled,
|
||||
configs: prev?.configs.map((config, i) => i === index ? result! : config) || [],
|
||||
}))
|
||||
}
|
||||
else {
|
||||
// add
|
||||
setDraftConfig(prev => ({
|
||||
...prev,
|
||||
enabled: !!prev?.enabled,
|
||||
configs: (prev?.configs || []).concat([{ ...result!, enabled: true }]),
|
||||
}))
|
||||
}
|
||||
},
|
||||
onRemoveCallback: ({ index }) => {
|
||||
if (index !== undefined && (draftConfig?.configs?.length ?? 0) > index) {
|
||||
setDraftConfig(prev => ({
|
||||
...prev,
|
||||
enabled: !!prev?.enabled,
|
||||
configs: prev?.configs.filter((_, i) => i !== index) || [],
|
||||
}))
|
||||
}
|
||||
},
|
||||
})
|
||||
}, [
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
draftConfig?.configs?.length,
|
||||
provider,
|
||||
setDraftConfig,
|
||||
setShowModelLoadBalancingEntryModal,
|
||||
])
|
||||
|
||||
const clearCountdown = useCallback((index: number) => {
|
||||
updateConfigEntry(index, ({ ttl: _, ...entry }) => {
|
||||
return {
|
||||
@@ -210,9 +194,21 @@ const ModelLoadBalancingConfigs = ({
|
||||
<div className='flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
|
||||
<span
|
||||
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
|
||||
onClick={() => toggleEntryModal(index, config)}
|
||||
onClick={() => {
|
||||
handleOpenModal(
|
||||
provider,
|
||||
configurationMethod,
|
||||
currentCustomConfigurationModelFixedFields,
|
||||
configurationMethod === ConfigurationMethodEnum.customizableModel,
|
||||
(config.credential_id && config.name) ? {
|
||||
credential_id: config.credential_id,
|
||||
credential_name: config.name,
|
||||
} : undefined,
|
||||
model,
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Edit02 className='h-4 w-4' />
|
||||
<RiEqualizer2Line className='h-4 w-4' />
|
||||
</span>
|
||||
<span
|
||||
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg bg-components-button-secondary-bg text-text-tertiary transition-colors hover:bg-components-button-secondary-bg-hover'
|
||||
@@ -234,20 +230,19 @@ const ModelLoadBalancingConfigs = ({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<div
|
||||
className='mt-1 flex h-8 items-center px-3 text-[13px] font-medium text-primary-600'
|
||||
onClick={() => toggleEntryModal()}
|
||||
>
|
||||
<div className='flex cursor-pointer items-center'>
|
||||
<Plus02 className='mr-2 h-3 w-3' />{t('common.modelProvider.addConfig')}
|
||||
</div>
|
||||
</div>
|
||||
<AddCredentialInLoadBalancing
|
||||
provider={provider}
|
||||
model={model}
|
||||
configurationMethod={configurationMethod}
|
||||
modelCredential={modelCredential}
|
||||
onSelectCredential={addConfigEntry}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
draftConfig.enabled && draftConfig.configs.length < 2 && (
|
||||
<div className='flex h-[34px] items-center border-t border-t-divider-subtle bg-components-panel-bg px-6 text-xs text-text-secondary'>
|
||||
<div className='flex h-[34px] items-center rounded-b-xl border-t border-t-divider-subtle bg-components-panel-bg px-6 text-xs text-text-secondary'>
|
||||
<AlertTriangle className='mr-1 h-3 w-3 text-[#f79009]' />
|
||||
{t('common.modelProvider.loadBalancingLeastKeyWarning')}
|
||||
</div>
|
||||
|
||||
@@ -1,40 +1,66 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import type { ModelItem, ModelLoadBalancingConfig, ModelLoadBalancingConfigEntry, ModelProvider } from '../declarations'
|
||||
import { FormTypeEnum } from '../declarations'
|
||||
import type {
|
||||
Credential,
|
||||
ModelItem,
|
||||
ModelLoadBalancingConfig,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
ModelProvider,
|
||||
} from '../declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
FormTypeEnum,
|
||||
} from '../declarations'
|
||||
import ModelIcon from '../model-icon'
|
||||
import ModelName from '../model-name'
|
||||
import { savePredefinedLoadBalancingConfig } from '../utils'
|
||||
import ModelLoadBalancingConfigs from './model-load-balancing-configs'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { fetchModelLoadBalancingConfig } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
// import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
|
||||
import {
|
||||
useGetModelCredential,
|
||||
useUpdateModelLoadBalancingConfig,
|
||||
} from '@/service/use-models'
|
||||
|
||||
export type ModelLoadBalancingModalProps = {
|
||||
provider: ModelProvider
|
||||
configurateMethod: ConfigurationMethodEnum
|
||||
model: ModelItem
|
||||
credential?: Credential
|
||||
open?: boolean
|
||||
onClose?: () => void
|
||||
onSave?: (provider: string) => void
|
||||
}
|
||||
|
||||
// model balancing config modal
|
||||
const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSave }: ModelLoadBalancingModalProps) => {
|
||||
const ModelLoadBalancingModal = ({
|
||||
provider,
|
||||
configurateMethod,
|
||||
model,
|
||||
credential,
|
||||
open = false,
|
||||
onClose,
|
||||
onSave,
|
||||
}: ModelLoadBalancingModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const { data, mutate } = useSWR(
|
||||
`/workspaces/current/model-providers/${provider.provider}/models/credentials?model=${model.model}&model_type=${model.model_type}`,
|
||||
fetchModelLoadBalancingConfig,
|
||||
)
|
||||
|
||||
const originalConfig = data?.load_balancing
|
||||
const providerFormSchemaPredefined = configurateMethod === ConfigurationMethodEnum.predefinedModel
|
||||
const configFrom = providerFormSchemaPredefined ? 'predefined-model' : 'custom-model'
|
||||
const {
|
||||
isLoading,
|
||||
data,
|
||||
refetch,
|
||||
} = useGetModelCredential(true, provider.provider, credential?.credential_id, model.model, model.model_type, configFrom)
|
||||
const modelCredential = data
|
||||
const {
|
||||
load_balancing,
|
||||
} = modelCredential ?? {}
|
||||
const originalConfig = load_balancing
|
||||
const [draftConfig, setDraftConfig] = useState<ModelLoadBalancingConfig>()
|
||||
const originalConfigMap = useMemo(() => {
|
||||
if (!originalConfig)
|
||||
@@ -75,25 +101,24 @@ const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSav
|
||||
return result
|
||||
}, [extendedSecretFormSchemas, originalConfigMap])
|
||||
|
||||
const { mutateAsync: updateModelLoadBalancingConfig } = useUpdateModelLoadBalancingConfig(provider.provider)
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await savePredefinedLoadBalancingConfig(
|
||||
provider.provider,
|
||||
({
|
||||
...(data?.credentials ?? {}),
|
||||
__model_type: model.model_type,
|
||||
__model_name: model.model,
|
||||
}),
|
||||
const res = await updateModelLoadBalancingConfig(
|
||||
{
|
||||
...draftConfig,
|
||||
enabled: Boolean(draftConfig?.enabled),
|
||||
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
|
||||
config_from: configFrom,
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
load_balancing: {
|
||||
...draftConfig,
|
||||
configs: draftConfig!.configs.map(encodeConfigEntrySecretValues),
|
||||
enabled: Boolean(draftConfig?.enabled),
|
||||
},
|
||||
},
|
||||
)
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
mutate()
|
||||
onSave?.(provider.provider)
|
||||
onClose?.()
|
||||
}
|
||||
@@ -110,7 +135,11 @@ const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSav
|
||||
className='w-[640px] max-w-none px-8 pt-8'
|
||||
title={
|
||||
<div className='pb-3 font-semibold'>
|
||||
<div className='h-[30px]'>{t('common.modelProvider.configLoadBalancing')}</div>
|
||||
<div className='h-[30px]'>{
|
||||
draftConfig?.enabled
|
||||
? t('common.modelProvider.auth.configLoadBalancing')
|
||||
: t('common.modelProvider.auth.configModel')
|
||||
}</div>
|
||||
{Boolean(model) && (
|
||||
<div className='flex h-5 items-center'>
|
||||
<ModelIcon
|
||||
@@ -152,20 +181,34 @@ const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSav
|
||||
<div className='text-sm text-text-secondary'>{t('common.modelProvider.providerManaged')}</div>
|
||||
<div className='text-xs text-text-tertiary'>{t('common.modelProvider.providerManagedDescription')}</div>
|
||||
</div>
|
||||
{/* <SwitchCredentialInLoadBalancing
|
||||
draftConfig={draftConfig}
|
||||
setDraftConfig={setDraftConfig}
|
||||
provider={provider}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModelLoadBalancingConfigs {...{
|
||||
draftConfig,
|
||||
setDraftConfig,
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields: {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
},
|
||||
configurationMethod: model.fetch_from,
|
||||
className: 'mt-2',
|
||||
}} />
|
||||
{
|
||||
modelCredential && (
|
||||
<ModelLoadBalancingConfigs {...{
|
||||
draftConfig,
|
||||
setDraftConfig,
|
||||
provider,
|
||||
currentCustomConfigurationModelFixedFields: {
|
||||
__model_name: model.model,
|
||||
__model_type: model.model_type,
|
||||
},
|
||||
configurationMethod: model.fetch_from,
|
||||
className: 'mt-2',
|
||||
modelCredential,
|
||||
onUpdate: refetch,
|
||||
model: {
|
||||
model: model.model,
|
||||
model_type: model.model_type,
|
||||
},
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex items-center justify-end gap-2'>
|
||||
@@ -176,6 +219,7 @@ const ModelLoadBalancingModal = ({ provider, model, open = false, onClose, onSav
|
||||
disabled={
|
||||
loading
|
||||
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
|
||||
|| isLoading
|
||||
}
|
||||
>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ValidatedStatus } from '../key-validator/declarations'
|
||||
import type {
|
||||
CredentialFormSchemaRadio,
|
||||
CredentialFormSchemaTextInput,
|
||||
FormValue,
|
||||
ModelLoadBalancingConfig,
|
||||
@@ -82,12 +81,14 @@ export const saveCredentials = async (predefined: boolean, provider: string, v:
|
||||
let body, url
|
||||
|
||||
if (predefined) {
|
||||
const { __authorization_name__, ...rest } = v
|
||||
body = {
|
||||
config_from: ConfigurationMethodEnum.predefinedModel,
|
||||
credentials: v,
|
||||
credentials: rest,
|
||||
load_balancing: loadBalancing,
|
||||
name: __authorization_name__,
|
||||
}
|
||||
url = `/workspaces/current/model-providers/${provider}`
|
||||
url = `/workspaces/current/model-providers/${provider}/credentials`
|
||||
}
|
||||
else {
|
||||
const { __model_name, __model_type, ...credentials } = v
|
||||
@@ -117,12 +118,17 @@ export const savePredefinedLoadBalancingConfig = async (provider: string, v: For
|
||||
return setModelProvider({ url, body })
|
||||
}
|
||||
|
||||
export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue) => {
|
||||
export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue, credentialId?: string) => {
|
||||
let url = ''
|
||||
let body
|
||||
|
||||
if (predefined) {
|
||||
url = `/workspaces/current/model-providers/${provider}`
|
||||
url = `/workspaces/current/model-providers/${provider}/credentials`
|
||||
if (credentialId) {
|
||||
body = {
|
||||
credential_id: credentialId,
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (v) {
|
||||
@@ -174,7 +180,7 @@ export const genModelTypeFormSchema = (modelTypes: ModelTypeEnum[]) => {
|
||||
show_on: [],
|
||||
}
|
||||
}),
|
||||
} as CredentialFormSchemaRadio
|
||||
} as any
|
||||
}
|
||||
|
||||
export const genModelNameFormSchema = (model?: Pick<CredentialFormSchemaTextInput, 'label' | 'placeholder'>) => {
|
||||
@@ -191,5 +197,5 @@ export const genModelNameFormSchema = (model?: Pick<CredentialFormSchemaTextInpu
|
||||
zh_Hans: '请输入模型名称',
|
||||
en_US: 'Please enter model name',
|
||||
},
|
||||
} as CredentialFormSchemaTextInput
|
||||
} as any
|
||||
}
|
||||
|
||||
@@ -135,6 +135,13 @@ const Item = ({
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
{
|
||||
credential.from_enterprise && (
|
||||
<Badge className='shrink-0'>
|
||||
Enterprise
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -172,7 +179,7 @@ const Item = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
!isOAuth && !disableEdit && (
|
||||
!isOAuth && !disableEdit && !credential.from_enterprise && (
|
||||
<Tooltip popupContent={t('common.operation.edit')}>
|
||||
<ActionButton
|
||||
disabled={disabled}
|
||||
@@ -194,7 +201,7 @@ const Item = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
!disableDelete && (
|
||||
!disableDelete && !credential.from_enterprise && (
|
||||
<Tooltip popupContent={t('common.operation.delete')}>
|
||||
<ActionButton
|
||||
className='hover:bg-transparent'
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import type { PluginPayload } from '@/app/components/plugins/plugin-auth/types'
|
||||
import {
|
||||
useDeletePluginCredentialHook,
|
||||
useSetPluginDefaultCredentialHook,
|
||||
useUpdatePluginCredentialHook,
|
||||
} from '../hooks/use-credential'
|
||||
|
||||
export const usePluginAuthAction = (
|
||||
pluginPayload: PluginPayload,
|
||||
onUpdate?: () => void,
|
||||
) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const pendingOperationCredentialId = useRef<string | null>(null)
|
||||
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
|
||||
const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload)
|
||||
const openConfirm = useCallback((credentialId?: string) => {
|
||||
if (credentialId)
|
||||
pendingOperationCredentialId.current = credentialId
|
||||
|
||||
setDeleteCredentialId(pendingOperationCredentialId.current)
|
||||
}, [])
|
||||
const closeConfirm = useCallback(() => {
|
||||
setDeleteCredentialId(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
}, [])
|
||||
const [doingAction, setDoingAction] = useState(false)
|
||||
const doingActionRef = useRef(doingAction)
|
||||
const handleSetDoingAction = useCallback((doing: boolean) => {
|
||||
doingActionRef.current = doing
|
||||
setDoingAction(doing)
|
||||
}, [])
|
||||
const [editValues, setEditValues] = useState<Record<string, any> | null>(null)
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
if (!pendingOperationCredentialId.current) {
|
||||
setDeleteCredentialId(null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
await deletePluginCredential({ credential_id: pendingOperationCredentialId.current })
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
onUpdate?.()
|
||||
setDeleteCredentialId(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
setEditValues(null)
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [deletePluginCredential, onUpdate, notify, t, handleSetDoingAction])
|
||||
const handleEdit = useCallback((id: string, values: Record<string, any>) => {
|
||||
pendingOperationCredentialId.current = id
|
||||
setEditValues(values)
|
||||
}, [])
|
||||
const handleRemove = useCallback(() => {
|
||||
setDeleteCredentialId(pendingOperationCredentialId.current)
|
||||
}, [])
|
||||
const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload)
|
||||
const handleSetDefault = useCallback(async (id: string) => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
await setPluginDefaultCredential(id)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
onUpdate?.()
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
|
||||
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
|
||||
const handleRename = useCallback(async (payload: {
|
||||
credential_id: string
|
||||
name: string
|
||||
}) => {
|
||||
if (doingActionRef.current)
|
||||
return
|
||||
try {
|
||||
handleSetDoingAction(true)
|
||||
await updatePluginCredential(payload)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
onUpdate?.()
|
||||
}
|
||||
finally {
|
||||
handleSetDoingAction(false)
|
||||
}
|
||||
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
|
||||
|
||||
return {
|
||||
doingAction,
|
||||
handleSetDoingAction,
|
||||
openConfirm,
|
||||
closeConfirm,
|
||||
deleteCredentialId,
|
||||
setDeleteCredentialId,
|
||||
handleConfirm,
|
||||
editValues,
|
||||
setEditValues,
|
||||
handleEdit,
|
||||
handleRemove,
|
||||
handleSetDefault,
|
||||
handleRename,
|
||||
pendingOperationCredentialId,
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,5 @@ export type Credential = {
|
||||
is_default: boolean
|
||||
credentials?: Record<string, any>
|
||||
isWorkspaceDefault?: boolean
|
||||
from_enterprise?: boolean
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ const ReasoningConfigForm: React.FC<Props> = ({
|
||||
className='h-8 grow'
|
||||
type='number'
|
||||
value={varInput?.value || ''}
|
||||
onChange={e => handleValueChange(variable, type)(e.target.value)}
|
||||
onChange={handleValueChange(variable, type)}
|
||||
placeholder={placeholder?.[language] || placeholder?.en_US}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,6 @@ const NormalForm = () => {
|
||||
const searchParams = useSearchParams()
|
||||
const consoleToken = decodeURIComponent(searchParams.get('access_token') || '')
|
||||
const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '')
|
||||
const redirectUrl = searchParams.get('redirect_url') || ''
|
||||
const message = decodeURIComponent(searchParams.get('message') || '')
|
||||
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@@ -38,22 +37,6 @@ const NormalForm = () => {
|
||||
if (consoleToken && refreshToken) {
|
||||
localStorage.setItem('console_token', consoleToken)
|
||||
localStorage.setItem('refresh_token', refreshToken)
|
||||
const pendingStr = localStorage.getItem('oauth_authorize_pending')
|
||||
if (redirectUrl) {
|
||||
router.replace(decodeURIComponent(redirectUrl))
|
||||
return
|
||||
}
|
||||
if (pendingStr) {
|
||||
try {
|
||||
const pending = JSON.parse(pendingStr)
|
||||
if (pending?.returnUrl) {
|
||||
localStorage.removeItem('oauth_authorize_pending')
|
||||
router.replace(pending.returnUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
router.replace('/apps')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
ModelLoadBalancingConfigEntry,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
@@ -54,9 +56,6 @@ const ExternalAPIModal = dynamic(() => import('@/app/components/datasets/externa
|
||||
const ModelLoadBalancingModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ModelLoadBalancingEntryModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const OpeningSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/conversation-opener/modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
@@ -79,6 +78,9 @@ export type ModelModalType = {
|
||||
currentProvider: ModelProvider
|
||||
currentConfigurationMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
isModelCredential?: boolean
|
||||
credential?: Credential
|
||||
model?: CustomModel
|
||||
}
|
||||
export type LoadBalancingEntryModalType = ModelModalType & {
|
||||
entry?: ModelLoadBalancingConfigEntry
|
||||
@@ -95,7 +97,6 @@ export type ModalContextState = {
|
||||
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
|
||||
setShowExternalKnowledgeAPIModal: Dispatch<SetStateAction<ModalState<CreateExternalAPIReq> | null>>
|
||||
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
|
||||
setShowModelLoadBalancingEntryModal: Dispatch<SetStateAction<ModalState<LoadBalancingEntryModalType> | null>>
|
||||
setShowOpeningModal: Dispatch<SetStateAction<ModalState<OpeningStatement & {
|
||||
promptVariables?: PromptVariable[]
|
||||
workflowVariables?: InputVar[]
|
||||
@@ -113,7 +114,6 @@ const ModalContext = createContext<ModalContextState>({
|
||||
setShowModelModal: noop,
|
||||
setShowExternalKnowledgeAPIModal: noop,
|
||||
setShowModelLoadBalancingModal: noop,
|
||||
setShowModelLoadBalancingEntryModal: noop,
|
||||
setShowOpeningModal: noop,
|
||||
setShowUpdatePluginModal: noop,
|
||||
})
|
||||
@@ -138,7 +138,6 @@ export const ModalContextProvider = ({
|
||||
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
|
||||
const [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] = useState<ModalState<CreateExternalAPIReq> | null>(null)
|
||||
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
|
||||
const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState<ModalState<LoadBalancingEntryModalType> | null>(null)
|
||||
const [showOpeningModal, setShowOpeningModal] = useState<ModalState<OpeningStatement & {
|
||||
promptVariables?: PromptVariable[]
|
||||
workflowVariables?: InputVar[]
|
||||
@@ -204,30 +203,12 @@ export const ModalContextProvider = ({
|
||||
setShowExternalKnowledgeAPIModal(null)
|
||||
}, [showExternalKnowledgeAPIModal])
|
||||
|
||||
const handleCancelModelLoadBalancingEntryModal = useCallback(() => {
|
||||
showModelLoadBalancingEntryModal?.onCancelCallback?.()
|
||||
setShowModelLoadBalancingEntryModal(null)
|
||||
}, [showModelLoadBalancingEntryModal])
|
||||
|
||||
const handleCancelOpeningModal = useCallback(() => {
|
||||
setShowOpeningModal(null)
|
||||
if (showOpeningModal?.onCancelCallback)
|
||||
showOpeningModal.onCancelCallback()
|
||||
}, [showOpeningModal])
|
||||
|
||||
const handleSaveModelLoadBalancingEntryModal = useCallback((entry: ModelLoadBalancingConfigEntry) => {
|
||||
showModelLoadBalancingEntryModal?.onSaveCallback?.({
|
||||
...showModelLoadBalancingEntryModal.payload,
|
||||
entry,
|
||||
})
|
||||
setShowModelLoadBalancingEntryModal(null)
|
||||
}, [showModelLoadBalancingEntryModal])
|
||||
|
||||
const handleRemoveModelLoadBalancingEntry = useCallback(() => {
|
||||
showModelLoadBalancingEntryModal?.onRemoveCallback?.(showModelLoadBalancingEntryModal.payload)
|
||||
setShowModelLoadBalancingEntryModal(null)
|
||||
}, [showModelLoadBalancingEntryModal])
|
||||
|
||||
const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtension) => {
|
||||
if (showApiBasedExtensionModal?.onSaveCallback)
|
||||
showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension)
|
||||
@@ -269,7 +250,6 @@ export const ModalContextProvider = ({
|
||||
setShowModelModal,
|
||||
setShowExternalKnowledgeAPIModal,
|
||||
setShowModelLoadBalancingModal,
|
||||
setShowModelLoadBalancingEntryModal,
|
||||
setShowOpeningModal,
|
||||
setShowUpdatePluginModal,
|
||||
}}>
|
||||
@@ -337,6 +317,9 @@ export const ModalContextProvider = ({
|
||||
provider={showModelModal.payload.currentProvider}
|
||||
configurateMethod={showModelModal.payload.currentConfigurationMethod}
|
||||
currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields}
|
||||
isModelCredential={showModelModal.payload.isModelCredential}
|
||||
credential={showModelModal.payload.credential}
|
||||
model={showModelModal.payload.model}
|
||||
onCancel={handleCancelModelModal}
|
||||
onSave={handleSaveModelModal}
|
||||
/>
|
||||
@@ -359,19 +342,6 @@ export const ModalContextProvider = ({
|
||||
<ModelLoadBalancingModal {...showModelLoadBalancingModal!} />
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showModelLoadBalancingEntryModal && (
|
||||
<ModelLoadBalancingEntryModal
|
||||
provider={showModelLoadBalancingEntryModal.payload.currentProvider}
|
||||
configurationMethod={showModelLoadBalancingEntryModal.payload.currentConfigurationMethod}
|
||||
currentCustomConfigurationModelFixedFields={showModelLoadBalancingEntryModal.payload.currentCustomConfigurationModelFixedFields}
|
||||
entry={showModelLoadBalancingEntryModal.payload.entry}
|
||||
onCancel={handleCancelModelLoadBalancingEntryModal}
|
||||
onSave={handleSaveModelLoadBalancingEntryModal}
|
||||
onRemove={handleRemoveModelLoadBalancingEntry}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{showOpeningModal && (
|
||||
<OpeningSettingModal
|
||||
data={showOpeningModal.payload}
|
||||
|
||||
@@ -715,10 +715,6 @@ const translation = {
|
||||
supportedFormats: 'Unterstützt PNG, JPG, JPEG, WEBP und GIF',
|
||||
},
|
||||
you: 'Du',
|
||||
avatar: {
|
||||
deleteTitle: 'Avatar entfernen',
|
||||
deleteDescription: 'Bist du sicher, dass du dein Profilbild entfernen möchtest? Dein Konto wird das standardmäßige Anfangs-Avatar verwenden.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -40,6 +40,7 @@ const translation = {
|
||||
deleteApp: 'Delete App',
|
||||
settings: 'Settings',
|
||||
setup: 'Setup',
|
||||
config: 'Config',
|
||||
getForFree: 'Get for free',
|
||||
reload: 'Reload',
|
||||
ok: 'OK',
|
||||
@@ -486,6 +487,18 @@ const translation = {
|
||||
discoverMore: 'Discover more in ',
|
||||
emptyProviderTitle: 'Model provider not set up',
|
||||
emptyProviderTip: 'Please install a model provider first.',
|
||||
auth: {
|
||||
unAuthorized: 'Unauthorized',
|
||||
authRemoved: 'Auth removed',
|
||||
apiKeys: 'API Keys',
|
||||
addApiKey: 'Add API Key',
|
||||
addNewModel: 'Add new model',
|
||||
addCredential: 'Add credential',
|
||||
addModelCredential: 'Add model credential',
|
||||
modelCredentials: 'Model credentials',
|
||||
configModel: 'Config model',
|
||||
configLoadBalancing: 'Config Load Balancing',
|
||||
},
|
||||
},
|
||||
dataSource: {
|
||||
add: 'Add a data source',
|
||||
@@ -709,10 +722,6 @@ const translation = {
|
||||
pagination: {
|
||||
perPage: 'Items per page',
|
||||
},
|
||||
avatar: {
|
||||
deleteTitle: 'Remove Avatar',
|
||||
deleteDescription: 'Are you sure you want to remove your profile picture? Your account will use the default initial avatar.',
|
||||
},
|
||||
imageInput: {
|
||||
dropImageHere: 'Drop your image here, or',
|
||||
browse: 'browse',
|
||||
|
||||
@@ -715,10 +715,6 @@ const translation = {
|
||||
dropImageHere: 'Deja tu imagen aquí, o',
|
||||
},
|
||||
you: 'Tú',
|
||||
avatar: {
|
||||
deleteTitle: 'Eliminar Avatar',
|
||||
deleteDescription: '¿Estás seguro de que deseas eliminar tu foto de perfil? Tu cuenta usará el avatar inicial predeterminado.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -716,10 +716,6 @@ const translation = {
|
||||
browse: 'مرورگر',
|
||||
},
|
||||
you: 'تو',
|
||||
avatar: {
|
||||
deleteTitle: 'حذف آواتار',
|
||||
deleteDescription: 'آیا مطمئن هستید که میخواهید تصویر پروفایل خود را حذف کنید؟ حساب شما از آواتار اولیه پیشفرض استفاده خواهد کرد.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -716,10 +716,6 @@ const translation = {
|
||||
supportedFormats: 'Prend en charge PNG, JPG, JPEG, WEBP et GIF',
|
||||
},
|
||||
you: 'Vous',
|
||||
avatar: {
|
||||
deleteTitle: 'Supprimer l\'avatar',
|
||||
deleteDescription: 'Êtes-vous sûr de vouloir supprimer votre photo de profil ? Votre compte utilisera l\'avatar par défaut.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -738,10 +738,6 @@ const translation = {
|
||||
dropImageHere: 'अपनी छवि यहाँ छोड़ें, या',
|
||||
},
|
||||
you: 'आप',
|
||||
avatar: {
|
||||
deleteTitle: 'अवतार हटाएँ',
|
||||
deleteDescription: 'क्या आप सुनिश्चित हैं कि आप अपनी प्रोफ़ाइल तस्वीर को हटाना चाहते हैं? आपका खाता डिफ़ॉल्ट प्रारंभिक अवतार का उपयोग करेगा।',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -747,10 +747,6 @@ const translation = {
|
||||
dropImageHere: 'Trascina la tua immagine qui, oppure',
|
||||
},
|
||||
you: 'Tu',
|
||||
avatar: {
|
||||
deleteTitle: 'Rimuovi avatar',
|
||||
deleteDescription: 'Sei sicuro di voler rimuovere la tua immagine del profilo? Il tuo account utilizzerà l\'avatar iniziale predefinito.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -715,10 +715,6 @@ const translation = {
|
||||
supportedFormats: 'PNG、JPG、JPEG、WEBP、および GIF をサポートしています。',
|
||||
dropImageHere: 'ここに画像をドロップするか、',
|
||||
},
|
||||
avatar: {
|
||||
deleteTitle: 'アバターを削除する',
|
||||
deleteDescription: '本当にプロフィール写真を削除してもよろしいですか?あなたのアカウントはデフォルトの初期アバターを使用します。',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -711,10 +711,6 @@ const translation = {
|
||||
dropImageHere: '여기에 이미지를 드롭하거나',
|
||||
},
|
||||
you: '너',
|
||||
avatar: {
|
||||
deleteTitle: '아바타 제거하기',
|
||||
deleteDescription: '프로필 사진을 제거하시겠습니까? 귀하의 계정은 기본 초기 아바타를 사용하게 됩니다.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -734,10 +734,6 @@ const translation = {
|
||||
supportedFormats: 'Obsługuje PNG, JPG, JPEG, WEBP i GIF',
|
||||
},
|
||||
you: 'Ty',
|
||||
avatar: {
|
||||
deleteTitle: 'Usuń awatar',
|
||||
deleteDescription: 'Czy na pewno chcesz usunąć swoje zdjęcie profilowe? Twoje konto będzie używać domyślnego, początkowego awatara.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -716,10 +716,6 @@ const translation = {
|
||||
browse: 'navegar',
|
||||
},
|
||||
you: 'Você',
|
||||
avatar: {
|
||||
deleteTitle: 'Remover Avatar',
|
||||
deleteDescription: 'Você tem certeza de que deseja remover sua foto de perfil? Sua conta usará o avatar padrão inicial.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -716,10 +716,6 @@ const translation = {
|
||||
dropImageHere: 'Trageți imaginea aici sau',
|
||||
},
|
||||
you: 'Tu',
|
||||
avatar: {
|
||||
deleteDescription: 'Ești sigur că vrei să îți ștergi fotografia de profil? Contul tău va folosi avatarul inițial implicit.',
|
||||
deleteTitle: 'Îndepărtează avatarul',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -716,10 +716,6 @@ const translation = {
|
||||
supportedFormats: 'Поддерживает PNG, JPG, JPEG, WEBP и GIF',
|
||||
},
|
||||
you: 'Ты',
|
||||
avatar: {
|
||||
deleteTitle: 'Удалить аватар',
|
||||
deleteDescription: 'Вы уверены, что хотите удалить свою фотографию профиля? Ваш аккаунт будет использовать стандартный аватар.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -914,10 +914,6 @@ const translation = {
|
||||
dropImageHere: 'Tukaj spustite svojo sliko ali',
|
||||
},
|
||||
you: 'Ti',
|
||||
avatar: {
|
||||
deleteTitle: 'Odstrani avatar',
|
||||
deleteDescription: 'Ali ste prepričani, da želite odstraniti svojo profilno sliko? Vaš račun bo uporabljal privzeti začetni avatar.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -711,10 +711,6 @@ const translation = {
|
||||
supportedFormats: 'รองรับ PNG, JPG, JPEG, WEBP และ GIF',
|
||||
},
|
||||
you: 'คุณ',
|
||||
avatar: {
|
||||
deleteTitle: 'ลบอวตาร',
|
||||
deleteDescription: 'คุณแน่ใจหรือไม่ว่าต้องการลบรูปโปรไฟล์ของคุณ? บัญชีของคุณจะใช้รูปโปรไฟล์เริ่มต้นตามค่าเริ่มต้น.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
@@ -716,10 +716,6 @@ const translation = {
|
||||
browse: 'tarayıcı',
|
||||
},
|
||||
you: 'Sen',
|
||||
avatar: {
|
||||
deleteTitle: 'Avatarı kaldır',
|
||||
deleteDescription: 'Profil resminizi kaldırmak istediğinize emin misiniz? Hesabınız varsayılan başlangıç avatarını kullanacaktır.',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user