mirror of
https://github.com/langgenius/dify.git
synced 2026-01-19 21:44:07 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee0d12455 | ||
|
|
13f2c90a7b | ||
|
|
a3dca3dabc | ||
|
|
e5c7a81ce3 | ||
|
|
8b0100523b | ||
|
|
1350599c0b | ||
|
|
bc54cdc537 | ||
|
|
5d10cf0fe6 | ||
|
|
7b8a10f3ea | ||
|
|
cb3a55dae6 | ||
|
|
5789d76582 | ||
|
|
2e588ae221 | ||
|
|
b5dd948e56 | ||
|
|
1263b7de75 | ||
|
|
75a6122173 | ||
|
|
053102f433 | ||
|
|
d3a2c0ed34 | ||
|
|
8fbc374f31 | ||
|
|
08b7ebba91 | ||
|
|
a1cd043fdc | ||
|
|
671a8e7972 | ||
|
|
efa16dbb44 | ||
|
|
a6241be42a | ||
|
|
faa88aafe8 | ||
|
|
1b3a98425f | ||
|
|
22bc9ddc73 | ||
|
|
0423775687 | ||
|
|
307c170fb6 | ||
|
|
0e04fcc071 | ||
|
|
4322b17a81 | ||
|
|
451af66be0 | ||
|
|
454577c6b1 | ||
|
|
53be4d2712 | ||
|
|
3c37fd37fa | ||
|
|
cf0ba794d7 | ||
|
|
c21e2063fe | ||
|
|
ad037c6615 | ||
|
|
7bbfac5dba | ||
|
|
80ddb00f10 | ||
|
|
74b2260ba6 | ||
|
|
603e55f252 | ||
|
|
a9c1c7d239 |
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -21,8 +21,8 @@ body:
|
||||
multiple: true
|
||||
options:
|
||||
- Cloud
|
||||
- Self Hosted
|
||||
- Other (please specify in "Steps to Reproduce")
|
||||
- Self Hosted (Docker)
|
||||
- Self Hosted (Source)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/langgenius/dify-web"></a>
|
||||
</p>
|
||||
|
||||
[v0.3.31:Surpassing the Assistants API – Dify's RAG Demonstrates an Impressive 20% Improvement.](https://dify.ai/blog/dify-ai-rag-technology-upgrade-performance-improvement-qa-accuracy)
|
||||
|
||||
**Dify** is an LLM application development platform that has already seen over **100,000** applications built on Dify.AI. It integrates the concepts of Backend as a Service and LLMOps, covering the core tech stack required for building generative AI-native applications, including a built-in RAG engine. With Dify, **you can self-deploy capabilities similar to Assistants API and GPTs based on any LLMs.**
|
||||
|
||||

|
||||
|
||||
@@ -124,5 +124,11 @@ HOSTED_ANTHROPIC_PAID_INCREASE_QUOTA=1000000
|
||||
HOSTED_ANTHROPIC_PAID_MIN_QUANTITY=20
|
||||
HOSTED_ANTHROPIC_PAID_MAX_QUANTITY=100
|
||||
|
||||
# Stripe configuration
|
||||
STRIPE_API_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# Billing configuration
|
||||
BILLING_API_URL=http://127.0.0.1:8000/v1
|
||||
BILLING_API_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_BILLING_SECRET=
|
||||
@@ -53,12 +53,3 @@
|
||||
```
|
||||
7. Setup your application by visiting http://localhost:5001/console/api/setup or other apis...
|
||||
8. If you need to debug local async processing, you can run `celery -A app.celery worker -P gevent -c 1 --loglevel INFO -Q dataset,generation,mail`, celery can do dataset importing and other async tasks.
|
||||
|
||||
8. Start frontend
|
||||
|
||||
You can start the frontend by running `npm install && npm run dev` in web/ folder, or you can use docker to start the frontend, for example:
|
||||
|
||||
```
|
||||
docker run -it -d --platform linux/amd64 -p 3000:3000 -e EDITION=SELF_HOSTED -e CONSOLE_URL=http://127.0.0.1:5001 --name web-self-hosted langgenius/dify-web:latest
|
||||
```
|
||||
This will start a dify frontend, now you are all set, happy coding!
|
||||
@@ -15,6 +15,7 @@ DEFAULTS = {
|
||||
'DB_HOST': 'localhost',
|
||||
'DB_PORT': '5432',
|
||||
'DB_DATABASE': 'dify',
|
||||
'DB_CHARSET': '',
|
||||
'REDIS_HOST': 'localhost',
|
||||
'REDIS_PORT': '6379',
|
||||
'REDIS_DB': '0',
|
||||
@@ -54,7 +55,6 @@ DEFAULTS = {
|
||||
'HOSTED_ANTHROPIC_PAID_MAX_QUANTITY': 100,
|
||||
'HOSTED_MODERATION_ENABLED': 'False',
|
||||
'HOSTED_MODERATION_PROVIDERS': '',
|
||||
'TENANT_DOCUMENT_COUNT': 100,
|
||||
'CLEAN_DAY_SETTING': 30,
|
||||
'UPLOAD_FILE_SIZE_LIMIT': 15,
|
||||
'UPLOAD_FILE_BATCH_LIMIT': 5,
|
||||
@@ -91,7 +91,7 @@ class Config:
|
||||
# ------------------------
|
||||
# General Configurations.
|
||||
# ------------------------
|
||||
self.CURRENT_VERSION = "0.3.32"
|
||||
self.CURRENT_VERSION = "0.3.33"
|
||||
self.COMMIT_SHA = get_env('COMMIT_SHA')
|
||||
self.EDITION = "SELF_HOSTED"
|
||||
self.DEPLOY_ENV = get_env('DEPLOY_ENV')
|
||||
@@ -149,10 +149,12 @@ class Config:
|
||||
# ------------------------
|
||||
db_credentials = {
|
||||
key: get_env(key) for key in
|
||||
['DB_USERNAME', 'DB_PASSWORD', 'DB_HOST', 'DB_PORT', 'DB_DATABASE']
|
||||
['DB_USERNAME', 'DB_PASSWORD', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_CHARSET']
|
||||
}
|
||||
|
||||
self.SQLALCHEMY_DATABASE_URI = f"postgresql://{db_credentials['DB_USERNAME']}:{db_credentials['DB_PASSWORD']}@{db_credentials['DB_HOST']}:{db_credentials['DB_PORT']}/{db_credentials['DB_DATABASE']}"
|
||||
db_extras = f"?client_encoding={db_credentials['DB_CHARSET']}" if db_credentials['DB_CHARSET'] else ""
|
||||
|
||||
self.SQLALCHEMY_DATABASE_URI = f"postgresql://{db_credentials['DB_USERNAME']}:{db_credentials['DB_PASSWORD']}@{db_credentials['DB_HOST']}:{db_credentials['DB_PORT']}/{db_credentials['DB_DATABASE']}{db_extras}"
|
||||
self.SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
'pool_size': int(get_env('SQLALCHEMY_POOL_SIZE')),
|
||||
'pool_recycle': int(get_env('SQLALCHEMY_POOL_RECYCLE'))
|
||||
@@ -240,7 +242,6 @@ class Config:
|
||||
self.MULTIMODAL_SEND_IMAGE_FORMAT = get_env('MULTIMODAL_SEND_IMAGE_FORMAT')
|
||||
|
||||
# Dataset Configurations.
|
||||
self.TENANT_DOCUMENT_COUNT = get_env('TENANT_DOCUMENT_COUNT')
|
||||
self.CLEAN_DAY_SETTING = get_env('CLEAN_DAY_SETTING')
|
||||
|
||||
# File upload Configurations.
|
||||
|
||||
@@ -28,3 +28,5 @@ from .universal_chat import chat, conversation, message, parameter, audio
|
||||
|
||||
# Import webhook controllers
|
||||
from .webhook import stripe
|
||||
|
||||
from .billing import billing
|
||||
|
||||
@@ -12,7 +12,7 @@ from constants.model_template import model_templates, demo_model_templates
|
||||
from controllers.console import api
|
||||
from controllers.console.app.error import AppNotFoundError, ProviderNotInitializeError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from core.model_providers.error import ProviderTokenNotInitError, LLMBadRequestError
|
||||
from core.model_providers.model_factory import ModelFactory
|
||||
from core.model_providers.model_provider_factory import ModelProviderFactory
|
||||
@@ -57,6 +57,7 @@ class AppListApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(app_detail_fields)
|
||||
@cloud_edition_billing_resource_check('apps')
|
||||
def post(self):
|
||||
"""Create app"""
|
||||
parser = reqparse.RequestParser()
|
||||
|
||||
@@ -161,7 +161,7 @@ class ChatMessageApi(Resource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
def compact_response(response: Union[dict | Generator]) -> Response:
|
||||
def compact_response(response: Union[dict, Generator]) -> Response:
|
||||
if isinstance(response, dict):
|
||||
return Response(response=json.dumps(response), status=200, mimetype='application/json')
|
||||
else:
|
||||
|
||||
@@ -249,7 +249,7 @@ class MessageMoreLikeThisApi(Resource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
def compact_response(response: Union[dict | Generator]) -> Response:
|
||||
def compact_response(response: Union[dict, Generator]) -> Response:
|
||||
if isinstance(response, dict):
|
||||
return Response(response=json.dumps(response), status=200, mimetype='application/json')
|
||||
else:
|
||||
|
||||
0
api/controllers/console/billing/__init__.py
Normal file
0
api/controllers/console/billing/__init__.py
Normal file
85
api/controllers/console/billing/billing.py
Normal file
85
api/controllers/console/billing/billing.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import stripe
|
||||
import os
|
||||
|
||||
from flask_restful import Resource, reqparse
|
||||
from flask_login import current_user
|
||||
from flask import current_app, request
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import only_edition_cloud
|
||||
from libs.login import login_required
|
||||
from services.billing_service import BillingService
|
||||
|
||||
|
||||
class BillingInfo(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
|
||||
edition = current_app.config['EDITION']
|
||||
if edition != 'CLOUD':
|
||||
return {"enabled": False}
|
||||
|
||||
return BillingService.get_info(current_user.current_tenant_id)
|
||||
|
||||
|
||||
class Subscription(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@only_edition_cloud
|
||||
def get(self):
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('plan', type=str, required=True, location='args', choices=['professional', 'team'])
|
||||
parser.add_argument('interval', type=str, required=True, location='args', choices=['month', 'year'])
|
||||
args = parser.parse_args()
|
||||
|
||||
return BillingService.get_subscription(args['plan'], args['interval'], current_user.email, current_user.name, current_user.current_tenant_id)
|
||||
|
||||
|
||||
class Invoices(Resource):
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@only_edition_cloud
|
||||
def get(self):
|
||||
|
||||
return BillingService.get_invoices(current_user.email)
|
||||
|
||||
|
||||
class StripeBillingWebhook(Resource):
|
||||
|
||||
@setup_required
|
||||
@only_edition_cloud
|
||||
def post(self):
|
||||
payload = request.data
|
||||
sig_header = request.headers.get('STRIPE_SIGNATURE')
|
||||
webhook_secret = os.environ.get('STRIPE_WEBHOOK_BILLING_SECRET', 'STRIPE_WEBHOOK_BILLING_SECRET')
|
||||
|
||||
try:
|
||||
event = stripe.Webhook.construct_event(
|
||||
payload, sig_header, webhook_secret
|
||||
)
|
||||
except ValueError as e:
|
||||
# Invalid payload
|
||||
return 'Invalid payload', 400
|
||||
except stripe.error.SignatureVerificationError as e:
|
||||
# Invalid signature
|
||||
return 'Invalid signature', 400
|
||||
|
||||
BillingService.process_event(event)
|
||||
|
||||
return 'success', 200
|
||||
|
||||
|
||||
api.add_resource(BillingInfo, '/billing/info')
|
||||
api.add_resource(Subscription, '/billing/subscription')
|
||||
api.add_resource(Invoices, '/billing/invoices')
|
||||
api.add_resource(StripeBillingWebhook, '/billing/webhook/stripe')
|
||||
@@ -493,3 +493,4 @@ api.add_resource(DatasetApiDeleteApi, '/datasets/api-keys/<uuid:api_key_id>')
|
||||
api.add_resource(DatasetApiBaseUrlApi, '/datasets/api-base-info')
|
||||
api.add_resource(DatasetRetrievalSettingApi, '/datasets/retrieval-setting')
|
||||
api.add_resource(DatasetRetrievalSettingMockApi, '/datasets/retrieval-setting/<string:vector_type>')
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from controllers.console.app.error import ProviderNotInitializeError, ProviderQu
|
||||
from controllers.console.datasets.error import DocumentAlreadyFinishedError, InvalidActionError, DocumentIndexingError, \
|
||||
InvalidMetadataError, ArchivedDocumentImmutableError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from core.indexing_runner import IndexingRunner
|
||||
from core.model_providers.error import ProviderTokenNotInitError, QuotaExceededError, ModelCurrentlyNotSupportError, \
|
||||
LLMBadRequestError
|
||||
@@ -194,6 +194,7 @@ class DatasetDocumentListApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(documents_and_batch_fields)
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
def post(self, dataset_id):
|
||||
dataset_id = str(dataset_id)
|
||||
|
||||
@@ -252,6 +253,7 @@ class DatasetInitApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@marshal_with(dataset_and_document_fields)
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
def post(self):
|
||||
# The role of the current user in the ta table must be admin or owner
|
||||
if current_user.current_tenant.current_role not in ['admin', 'owner']:
|
||||
@@ -693,6 +695,7 @@ class DocumentStatusApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
def patch(self, dataset_id, document_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
document_id = str(document_id)
|
||||
@@ -770,14 +773,6 @@ class DocumentStatusApi(DocumentResource):
|
||||
if not document.archived:
|
||||
raise InvalidActionError('Document is not archived.')
|
||||
|
||||
# check document limit
|
||||
if current_app.config['EDITION'] == 'CLOUD':
|
||||
documents_count = DocumentService.get_tenant_documents_count()
|
||||
total_count = documents_count + 1
|
||||
tenant_document_count = int(current_app.config['TENANT_DOCUMENT_COUNT'])
|
||||
if total_count > tenant_document_count:
|
||||
raise ValueError(f"All your documents have overed limit {tenant_document_count}.")
|
||||
|
||||
document.archived = False
|
||||
document.archived_at = None
|
||||
document.archived_by = None
|
||||
@@ -856,21 +851,6 @@ class DocumentRecoverApi(DocumentResource):
|
||||
return {'result': 'success'}, 204
|
||||
|
||||
|
||||
class DocumentLimitApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
def get(self):
|
||||
"""get document limit"""
|
||||
documents_count = DocumentService.get_tenant_documents_count()
|
||||
tenant_document_count = int(current_app.config['TENANT_DOCUMENT_COUNT'])
|
||||
|
||||
return {
|
||||
'documents_count': documents_count,
|
||||
'documents_limit': tenant_document_count
|
||||
}, 200
|
||||
|
||||
|
||||
api.add_resource(GetProcessRuleApi, '/datasets/process-rule')
|
||||
api.add_resource(DatasetDocumentListApi,
|
||||
'/datasets/<uuid:dataset_id>/documents')
|
||||
@@ -896,4 +876,3 @@ api.add_resource(DocumentStatusApi,
|
||||
'/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/status/<string:action>')
|
||||
api.add_resource(DocumentPauseApi, '/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause')
|
||||
api.add_resource(DocumentRecoverApi, '/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume')
|
||||
api.add_resource(DocumentLimitApi, '/datasets/limit')
|
||||
|
||||
@@ -11,7 +11,7 @@ from controllers.console import api
|
||||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import InvalidActionError, NoFileUploadedError, TooManyFilesError
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from core.model_providers.error import LLMBadRequestError, ProviderTokenNotInitError
|
||||
from core.model_providers.model_factory import ModelFactory
|
||||
from libs.login import login_required
|
||||
@@ -114,6 +114,7 @@ class DatasetDocumentSegmentApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
def patch(self, dataset_id, segment_id, action):
|
||||
dataset_id = str(dataset_id)
|
||||
dataset = DatasetService.get_dataset(dataset_id)
|
||||
@@ -200,6 +201,7 @@ class DatasetDocumentSegmentAddApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
def post(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -250,6 +252,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
def patch(self, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
@@ -344,6 +347,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('vector_space')
|
||||
def post(self, dataset_id, document_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
|
||||
@@ -154,7 +154,7 @@ class ChatStopApi(InstalledAppResource):
|
||||
return {'result': 'success'}, 200
|
||||
|
||||
|
||||
def compact_response(response: Union[dict | Generator]) -> Response:
|
||||
def compact_response(response: Union[dict, Generator]) -> Response:
|
||||
if isinstance(response, dict):
|
||||
return Response(response=json.dumps(response), status=200, mimetype='application/json')
|
||||
else:
|
||||
|
||||
@@ -14,6 +14,7 @@ from extensions.ext_database import db
|
||||
from fields.installed_app_fields import installed_app_list_fields
|
||||
from models.model import App, InstalledApp, RecommendedApp
|
||||
from services.account_service import TenantService
|
||||
from controllers.console.wraps import cloud_edition_billing_resource_check
|
||||
|
||||
|
||||
class InstalledAppsListApi(Resource):
|
||||
@@ -47,6 +48,7 @@ class InstalledAppsListApi(Resource):
|
||||
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('apps')
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('app_id', type=str, required=True, help='Invalid app_id')
|
||||
|
||||
@@ -105,7 +105,7 @@ class MessageMoreLikeThisApi(InstalledAppResource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
def compact_response(response: Union[dict | Generator]) -> Response:
|
||||
def compact_response(response: Union[dict, Generator]) -> Response:
|
||||
if isinstance(response, dict):
|
||||
return Response(response=json.dumps(response), status=200, mimetype='application/json')
|
||||
else:
|
||||
|
||||
@@ -104,7 +104,7 @@ class UniversalChatStopApi(UniversalChatResource):
|
||||
return {'result': 'success'}, 200
|
||||
|
||||
|
||||
def compact_response(response: Union[dict | Generator]) -> Response:
|
||||
def compact_response(response: Union[dict, Generator]) -> Response:
|
||||
if isinstance(response, dict):
|
||||
return Response(response=json.dumps(response), status=200, mimetype='application/json')
|
||||
else:
|
||||
|
||||
@@ -7,7 +7,7 @@ from flask_restful import Resource, reqparse, marshal_with, abort, fields, marsh
|
||||
import services
|
||||
from controllers.console import api
|
||||
from controllers.console.setup import setup_required
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
||||
from libs.helper import TimestampField
|
||||
from extensions.ext_database import db
|
||||
from models.account import Account, TenantAccountJoin
|
||||
@@ -47,6 +47,7 @@ class MemberInviteEmailApi(Resource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@cloud_edition_billing_resource_check('members')
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('emails', type=str, required=True, location='json', action='append')
|
||||
|
||||
@@ -5,6 +5,7 @@ from flask import current_app, abort
|
||||
from flask_login import current_user
|
||||
|
||||
from controllers.console.workspace.error import AccountNotInitializedError
|
||||
from services.billing_service import BillingService
|
||||
|
||||
|
||||
def account_initialization_required(view):
|
||||
@@ -41,3 +42,30 @@ def only_edition_self_hosted(view):
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def cloud_edition_billing_resource_check(resource: str,
|
||||
error_msg: str = "You have reached the limit of your subscription."):
|
||||
def interceptor(view):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
if current_app.config['EDITION'] == 'CLOUD':
|
||||
tenant_id = current_user.current_tenant_id
|
||||
billing_info = BillingService.get_info(tenant_id)
|
||||
members = billing_info['members']
|
||||
apps = billing_info['apps']
|
||||
vector_space = billing_info['vector_space']
|
||||
|
||||
if resource == 'members' and 0 < members['limit'] <= members['size']:
|
||||
abort(403, error_msg)
|
||||
elif resource == 'apps' and 0 < apps['limit'] <= apps['size']:
|
||||
abort(403, error_msg)
|
||||
elif resource == 'vector_space' and 0 < vector_space['limit'] <= vector_space['size']:
|
||||
abort(403, error_msg)
|
||||
else:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return view(*args, **kwargs)
|
||||
return decorated
|
||||
return interceptor
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ class ChatStopApi(AppApiResource):
|
||||
return {'result': 'success'}, 200
|
||||
|
||||
|
||||
def compact_response(response: Union[dict | Generator]) -> Response:
|
||||
def compact_response(response: Union[dict, Generator]) -> Response:
|
||||
if isinstance(response, dict):
|
||||
return Response(response=json.dumps(response), status=200, mimetype='application/json')
|
||||
else:
|
||||
|
||||
@@ -11,7 +11,7 @@ from controllers.service_api import api
|
||||
from controllers.service_api.app.error import ProviderNotInitializeError
|
||||
from controllers.service_api.dataset.error import ArchivedDocumentImmutableError, DocumentIndexingError, \
|
||||
NoFileUploadedError, TooManyFilesError
|
||||
from controllers.service_api.wraps import DatasetApiResource
|
||||
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check
|
||||
from libs.login import current_user
|
||||
from core.model_providers.error import ProviderTokenNotInitError
|
||||
from extensions.ext_database import db
|
||||
@@ -24,6 +24,7 @@ from services.file_service import FileService
|
||||
class DocumentAddByTextApi(DatasetApiResource):
|
||||
"""Resource for documents."""
|
||||
|
||||
@cloud_edition_billing_resource_check('vector_space', 'dataset')
|
||||
def post(self, tenant_id, dataset_id):
|
||||
"""Create document by text."""
|
||||
parser = reqparse.RequestParser()
|
||||
@@ -88,6 +89,7 @@ class DocumentAddByTextApi(DatasetApiResource):
|
||||
class DocumentUpdateByTextApi(DatasetApiResource):
|
||||
"""Resource for update documents."""
|
||||
|
||||
@cloud_edition_billing_resource_check('vector_space', 'dataset')
|
||||
def post(self, tenant_id, dataset_id, document_id):
|
||||
"""Update document by text."""
|
||||
parser = reqparse.RequestParser()
|
||||
@@ -147,6 +149,7 @@ class DocumentUpdateByTextApi(DatasetApiResource):
|
||||
|
||||
class DocumentAddByFileApi(DatasetApiResource):
|
||||
"""Resource for documents."""
|
||||
@cloud_edition_billing_resource_check('vector_space', 'dataset')
|
||||
def post(self, tenant_id, dataset_id):
|
||||
"""Create document by upload file."""
|
||||
args = {}
|
||||
@@ -212,6 +215,7 @@ class DocumentAddByFileApi(DatasetApiResource):
|
||||
class DocumentUpdateByFileApi(DatasetApiResource):
|
||||
"""Resource for update documents."""
|
||||
|
||||
@cloud_edition_billing_resource_check('vector_space', 'dataset')
|
||||
def post(self, tenant_id, dataset_id, document_id):
|
||||
"""Update document by upload file."""
|
||||
args = {}
|
||||
|
||||
@@ -3,7 +3,7 @@ from flask_restful import reqparse, marshal
|
||||
from werkzeug.exceptions import NotFound
|
||||
from controllers.service_api import api
|
||||
from controllers.service_api.app.error import ProviderNotInitializeError
|
||||
from controllers.service_api.wraps import DatasetApiResource
|
||||
from controllers.service_api.wraps import DatasetApiResource, cloud_edition_billing_resource_check
|
||||
from core.model_providers.error import ProviderTokenNotInitError, LLMBadRequestError
|
||||
from core.model_providers.model_factory import ModelFactory
|
||||
from extensions.ext_database import db
|
||||
@@ -14,6 +14,8 @@ from services.dataset_service import DatasetService, DocumentService, SegmentSer
|
||||
|
||||
class SegmentApi(DatasetApiResource):
|
||||
"""Resource for segments."""
|
||||
|
||||
@cloud_edition_billing_resource_check('vector_space', 'dataset')
|
||||
def post(self, tenant_id, dataset_id, document_id):
|
||||
"""Create single segment."""
|
||||
# check dataset
|
||||
@@ -144,6 +146,7 @@ class DatasetSegmentApi(DatasetApiResource):
|
||||
SegmentService.delete_segment(segment, document, dataset)
|
||||
return {'result': 'success'}, 200
|
||||
|
||||
@cloud_edition_billing_resource_check('vector_space', 'dataset')
|
||||
def post(self, tenant_id, dataset_id, document_id, segment_id):
|
||||
# check dataset
|
||||
dataset_id = str(dataset_id)
|
||||
|
||||
@@ -11,6 +11,7 @@ from libs.login import _get_user
|
||||
from extensions.ext_database import db
|
||||
from models.account import Tenant, TenantAccountJoin, Account
|
||||
from models.model import ApiToken, App
|
||||
from services.billing_service import BillingService
|
||||
|
||||
|
||||
def validate_app_token(view=None):
|
||||
@@ -40,6 +41,33 @@ def validate_app_token(view=None):
|
||||
return decorator
|
||||
|
||||
|
||||
def cloud_edition_billing_resource_check(resource: str,
|
||||
api_token_type: str,
|
||||
error_msg: str = "You have reached the limit of your subscription."):
|
||||
def interceptor(view):
|
||||
def decorated(*args, **kwargs):
|
||||
if current_app.config['EDITION'] == 'CLOUD':
|
||||
api_token = validate_and_get_api_token(api_token_type)
|
||||
billing_info = BillingService.get_info(api_token.tenant_id)
|
||||
|
||||
members = billing_info['members']
|
||||
apps = billing_info['apps']
|
||||
vector_space = billing_info['vector_space']
|
||||
|
||||
if resource == 'members' and 0 < members['limit'] <= members['size']:
|
||||
raise Unauthorized(error_msg)
|
||||
elif resource == 'apps' and 0 < apps['limit'] <= apps['size']:
|
||||
raise Unauthorized(error_msg)
|
||||
elif resource == 'vector_space' and 0 < vector_space['limit'] <= vector_space['size']:
|
||||
raise Unauthorized(error_msg)
|
||||
else:
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return view(*args, **kwargs)
|
||||
return decorated
|
||||
return interceptor
|
||||
|
||||
|
||||
def validate_dataset_token(view=None):
|
||||
def decorator(view):
|
||||
@wraps(view)
|
||||
|
||||
@@ -68,7 +68,7 @@ class ConversationRenameApi(WebApiResource):
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument('name', type=str, required=False, location='json')
|
||||
parser.add_argument('auto_generate', type=bool, required=False, default='False', location='json')
|
||||
parser.add_argument('auto_generate', type=bool, required=False, default=False, location='json')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
|
||||
@@ -139,7 +139,7 @@ class MessageMoreLikeThisApi(WebApiResource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
def compact_response(response: Union[dict | Generator]) -> Response:
|
||||
def compact_response(response: Union[dict, Generator]) -> Response:
|
||||
if isinstance(response, dict):
|
||||
return Response(response=json.dumps(response), status=200, mimetype='application/json')
|
||||
else:
|
||||
|
||||
@@ -40,7 +40,7 @@ def decode_jwt_token():
|
||||
site = db.session.query(Site).filter(Site.code == app_code).first()
|
||||
if not app_model:
|
||||
raise NotFound()
|
||||
if not app_code and not site:
|
||||
if not app_code or not site:
|
||||
raise Unauthorized('Site URL is no longer valid.')
|
||||
if app_model.enable_site is False:
|
||||
raise Unauthorized('Site is disabled.')
|
||||
|
||||
@@ -59,7 +59,7 @@ class AgentExecutor:
|
||||
self.configuration = configuration
|
||||
self.agent = self._init_agent()
|
||||
|
||||
def _init_agent(self) -> Union[BaseSingleActionAgent | BaseMultiActionAgent]:
|
||||
def _init_agent(self) -> Union[BaseSingleActionAgent, BaseMultiActionAgent]:
|
||||
if self.configuration.strategy == PlanningStrategy.REACT:
|
||||
agent = AutoSummarizingStructuredChatAgent.from_llm_and_tools(
|
||||
model_instance=self.configuration.model_instance,
|
||||
|
||||
@@ -321,7 +321,7 @@ class ConversationMessageTask:
|
||||
|
||||
|
||||
class PubHandler:
|
||||
def __init__(self, user: Union[Account | EndUser], task_id: str,
|
||||
def __init__(self, user: Union[Account, EndUser], task_id: str,
|
||||
message: Message, conversation: Conversation,
|
||||
chain_pub: bool = False, agent_thought_pub: bool = False):
|
||||
self._channel = PubHandler.generate_channel_name(user, task_id)
|
||||
@@ -334,7 +334,7 @@ class PubHandler:
|
||||
self._agent_thought_pub = agent_thought_pub
|
||||
|
||||
@classmethod
|
||||
def generate_channel_name(cls, user: Union[Account | EndUser], task_id: str):
|
||||
def generate_channel_name(cls, user: Union[Account, EndUser], task_id: str):
|
||||
if not user:
|
||||
raise ValueError("user is required")
|
||||
|
||||
@@ -342,7 +342,7 @@ class PubHandler:
|
||||
return "generate_result:{}-{}".format(user_str, task_id)
|
||||
|
||||
@classmethod
|
||||
def generate_stopped_cache_key(cls, user: Union[Account | EndUser], task_id: str):
|
||||
def generate_stopped_cache_key(cls, user: Union[Account, EndUser], task_id: str):
|
||||
user_str = 'account-' + str(user.id) if isinstance(user, Account) else 'end-user-' + str(user.id)
|
||||
return "generate_result_stopped:{}-{}".format(user_str, task_id)
|
||||
|
||||
@@ -454,7 +454,7 @@ class PubHandler:
|
||||
redis_client.publish(self._channel, json.dumps(content))
|
||||
|
||||
@classmethod
|
||||
def pub_error(cls, user: Union[Account | EndUser], task_id: str, e):
|
||||
def pub_error(cls, user: Union[Account, EndUser], task_id: str, e):
|
||||
content = {
|
||||
'error': type(e).__name__,
|
||||
'description': e.description if getattr(e, 'description', None) is not None else str(e)
|
||||
@@ -467,7 +467,7 @@ class PubHandler:
|
||||
return redis_client.get(self._stopped_cache_key) is not None
|
||||
|
||||
@classmethod
|
||||
def ping(cls, user: Union[Account | EndUser], task_id: str):
|
||||
def ping(cls, user: Union[Account, EndUser], task_id: str):
|
||||
content = {
|
||||
'event': 'ping'
|
||||
}
|
||||
@@ -476,7 +476,7 @@ class PubHandler:
|
||||
redis_client.publish(channel, json.dumps(content))
|
||||
|
||||
@classmethod
|
||||
def stop(cls, user: Union[Account | EndUser], task_id: str):
|
||||
def stop(cls, user: Union[Account, EndUser], task_id: str):
|
||||
stopped_cache_key = cls.generate_stopped_cache_key(user, task_id)
|
||||
redis_client.setex(stopped_cache_key, 600, 1)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from extensions.ext_database import db
|
||||
from models.dataset import Dataset, DocumentSegment
|
||||
|
||||
|
||||
class DatesetDocumentStore:
|
||||
class DatasetDocumentStore:
|
||||
def __init__(
|
||||
self,
|
||||
dataset: Dataset,
|
||||
@@ -20,7 +20,7 @@ class DatesetDocumentStore:
|
||||
self._document_id = document_id
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, config_dict: Dict[str, Any]) -> "DatesetDocumentStore":
|
||||
def from_dict(cls, config_dict: Dict[str, Any]) -> "DatasetDocumentStore":
|
||||
return cls(**config_dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
|
||||
@@ -18,31 +18,30 @@ class CacheEmbedding(Embeddings):
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
"""Embed search docs."""
|
||||
# use doc embedding cache or store if not exists
|
||||
text_embeddings = []
|
||||
embedding_queue_texts = []
|
||||
for text in texts:
|
||||
text_embeddings = [None for _ in range(len(texts))]
|
||||
embedding_queue_indices = []
|
||||
for i, text in enumerate(texts):
|
||||
hash = helper.generate_text_hash(text)
|
||||
embedding = db.session.query(Embedding).filter_by(model_name=self._embeddings.name, hash=hash).first()
|
||||
if embedding:
|
||||
text_embeddings.append(embedding.get_embedding())
|
||||
text_embeddings[i] = embedding.get_embedding()
|
||||
else:
|
||||
embedding_queue_texts.append(text)
|
||||
embedding_queue_indices.append(i)
|
||||
|
||||
if embedding_queue_texts:
|
||||
if embedding_queue_indices:
|
||||
try:
|
||||
embedding_results = self._embeddings.client.embed_documents(embedding_queue_texts)
|
||||
embedding_results = self._embeddings.client.embed_documents([texts[i] for i in embedding_queue_indices])
|
||||
except Exception as ex:
|
||||
raise self._embeddings.handle_exceptions(ex)
|
||||
i = 0
|
||||
normalized_embedding_results = []
|
||||
for text in embedding_queue_texts:
|
||||
hash = helper.generate_text_hash(text)
|
||||
|
||||
for i, indice in enumerate(embedding_queue_indices):
|
||||
hash = helper.generate_text_hash(texts[indice])
|
||||
|
||||
try:
|
||||
embedding = Embedding(model_name=self._embeddings.name, hash=hash)
|
||||
vector = embedding_results[i]
|
||||
normalized_embedding = (vector / np.linalg.norm(vector)).tolist()
|
||||
normalized_embedding_results.append(normalized_embedding)
|
||||
text_embeddings[indice] = normalized_embedding
|
||||
embedding.set_embedding(normalized_embedding)
|
||||
db.session.add(embedding)
|
||||
db.session.commit()
|
||||
@@ -52,10 +51,7 @@ class CacheEmbedding(Embeddings):
|
||||
except:
|
||||
logging.exception('Failed to add embedding to db')
|
||||
continue
|
||||
finally:
|
||||
i += 1
|
||||
|
||||
text_embeddings.extend(normalized_embedding_results)
|
||||
return text_embeddings
|
||||
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
|
||||
@@ -15,7 +15,7 @@ from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
|
||||
from core.data_loader.file_extractor import FileExtractor
|
||||
from core.data_loader.loader.notion import NotionLoader
|
||||
from core.docstore.dataset_docstore import DatesetDocumentStore
|
||||
from core.docstore.dataset_docstore import DatasetDocumentStore
|
||||
from core.generator.llm_generator import LLMGenerator
|
||||
from core.index.index import IndexBuilder
|
||||
from core.model_providers.error import ProviderTokenNotInitError
|
||||
@@ -106,7 +106,8 @@ class IndexingRunner:
|
||||
document_id=dataset_document.id
|
||||
).all()
|
||||
|
||||
db.session.delete(document_segments)
|
||||
for document_segment in document_segments:
|
||||
db.session.delete(document_segment)
|
||||
db.session.commit()
|
||||
|
||||
# load file
|
||||
@@ -474,7 +475,7 @@ class IndexingRunner:
|
||||
)
|
||||
|
||||
# save node to document segment
|
||||
doc_store = DatesetDocumentStore(
|
||||
doc_store = DatasetDocumentStore(
|
||||
dataset=dataset,
|
||||
user_id=dataset_document.created_by,
|
||||
document_id=dataset_document.id
|
||||
|
||||
@@ -75,6 +75,9 @@ class ModelProviderFactory:
|
||||
elif provider_name == 'cohere':
|
||||
from core.model_providers.providers.cohere_provider import CohereProvider
|
||||
return CohereProvider
|
||||
elif provider_name == 'jina':
|
||||
from core.model_providers.providers.jina_provider import JinaProvider
|
||||
return JinaProvider
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
25
api/core/model_providers/models/embedding/jina_embedding.py
Normal file
25
api/core/model_providers/models/embedding/jina_embedding.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from core.model_providers.error import LLMBadRequestError
|
||||
from core.model_providers.models.embedding.base import BaseEmbedding
|
||||
from core.model_providers.providers.base import BaseModelProvider
|
||||
from core.third_party.langchain.embeddings.jina_embedding import JinaEmbeddings
|
||||
|
||||
|
||||
class JinaEmbedding(BaseEmbedding):
|
||||
def __init__(self, model_provider: BaseModelProvider, name: str):
|
||||
credentials = model_provider.get_model_credentials(
|
||||
model_name=name,
|
||||
model_type=self.type
|
||||
)
|
||||
|
||||
client = JinaEmbeddings(
|
||||
model=name,
|
||||
**credentials
|
||||
)
|
||||
|
||||
super().__init__(model_provider, client, name)
|
||||
|
||||
def handle_exceptions(self, ex: Exception) -> Exception:
|
||||
if isinstance(ex, ValueError):
|
||||
return LLMBadRequestError(f"Jina: {str(ex)}")
|
||||
else:
|
||||
return ex
|
||||
@@ -1,14 +1,15 @@
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
from typing import List, Optional
|
||||
|
||||
import cohere
|
||||
import openai
|
||||
from langchain.schema import Document
|
||||
|
||||
from core.model_providers.error import LLMBadRequestError, LLMAPIConnectionError, LLMAPIUnavailableError, \
|
||||
LLMRateLimitError, LLMAuthorizationError
|
||||
from core.model_providers.error import (LLMAPIConnectionError,
|
||||
LLMAPIUnavailableError,
|
||||
LLMAuthorizationError,
|
||||
LLMBadRequestError, LLMRateLimitError)
|
||||
from core.model_providers.models.reranking.base import BaseReranking
|
||||
from core.model_providers.providers.base import BaseModelProvider
|
||||
from langchain.schema import Document
|
||||
|
||||
|
||||
class CohereReranking(BaseReranking):
|
||||
@@ -26,10 +27,14 @@ class CohereReranking(BaseReranking):
|
||||
def rerank(self, query: str, documents: List[Document], score_threshold: Optional[float], top_k: Optional[int]) -> Optional[List[Document]]:
|
||||
docs = []
|
||||
doc_id = []
|
||||
unique_documents = []
|
||||
for document in documents:
|
||||
if document.metadata['doc_id'] not in doc_id:
|
||||
doc_id.append(document.metadata['doc_id'])
|
||||
docs.append(document.page_content)
|
||||
unique_documents.append(document)
|
||||
documents = unique_documents
|
||||
|
||||
results = self.client.rerank(query=query, documents=docs, model=self.name, top_n=top_k)
|
||||
rerank_documents = []
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
from langchain.schema import Document
|
||||
from xinference_client.client.restful.restful_client import Client
|
||||
from typing import List, Optional
|
||||
|
||||
from core.model_providers.error import LLMBadRequestError
|
||||
from core.model_providers.models.reranking.base import BaseReranking
|
||||
from core.model_providers.providers.base import BaseModelProvider
|
||||
from langchain.schema import Document
|
||||
from xinference_client.client.restful.restful_client import Client
|
||||
|
||||
|
||||
class XinferenceReranking(BaseReranking):
|
||||
@@ -24,11 +23,14 @@ class XinferenceReranking(BaseReranking):
|
||||
def rerank(self, query: str, documents: List[Document], score_threshold: Optional[float], top_k: Optional[int]) -> Optional[List[Document]]:
|
||||
docs = []
|
||||
doc_id = []
|
||||
unique_documents = []
|
||||
for document in documents:
|
||||
if document.metadata['doc_id'] not in doc_id:
|
||||
doc_id.append(document.metadata['doc_id'])
|
||||
docs.append(document.page_content)
|
||||
|
||||
unique_documents.append(document)
|
||||
documents = unique_documents
|
||||
|
||||
model = self.client.get_model(self.credentials['model_uid'])
|
||||
response = model.rerank(query=query, documents=docs, top_n=top_k)
|
||||
rerank_documents = []
|
||||
@@ -48,7 +50,7 @@ class XinferenceReranking(BaseReranking):
|
||||
)
|
||||
# score threshold check
|
||||
if score_threshold is not None:
|
||||
if result.relevance_score >= score_threshold:
|
||||
if result['relevance_score'] >= score_threshold:
|
||||
rerank_documents.append(rerank_document)
|
||||
else:
|
||||
rerank_documents.append(rerank_document)
|
||||
|
||||
141
api/core/model_providers/providers/jina_provider.py
Normal file
141
api/core/model_providers/providers/jina_provider.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
from typing import Type
|
||||
|
||||
from core.helper import encrypter
|
||||
from core.model_providers.models.base import BaseProviderModel
|
||||
from core.model_providers.models.embedding.jina_embedding import JinaEmbedding
|
||||
from core.model_providers.models.entity.model_params import ModelType, ModelKwargsRules
|
||||
from core.model_providers.providers.base import BaseModelProvider, CredentialsValidateFailedError
|
||||
from core.third_party.langchain.embeddings.jina_embedding import JinaEmbeddings
|
||||
from models.provider import ProviderType
|
||||
|
||||
|
||||
class JinaProvider(BaseModelProvider):
|
||||
|
||||
@property
|
||||
def provider_name(self):
|
||||
"""
|
||||
Returns the name of a provider.
|
||||
"""
|
||||
return 'jina'
|
||||
|
||||
def _get_fixed_model_list(self, model_type: ModelType) -> list[dict]:
|
||||
if model_type == ModelType.EMBEDDINGS:
|
||||
return [
|
||||
{
|
||||
'id': 'jina-embeddings-v2-base-en',
|
||||
'name': 'jina-embeddings-v2-base-en',
|
||||
},
|
||||
{
|
||||
'id': 'jina-embeddings-v2-small-en',
|
||||
'name': 'jina-embeddings-v2-small-en',
|
||||
}
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_model_class(self, model_type: ModelType) -> Type[BaseProviderModel]:
|
||||
"""
|
||||
Returns the model class.
|
||||
|
||||
:param model_type:
|
||||
:return:
|
||||
"""
|
||||
if model_type == ModelType.EMBEDDINGS:
|
||||
model_class = JinaEmbedding
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return model_class
|
||||
|
||||
@classmethod
|
||||
def is_provider_credentials_valid_or_raise(cls, credentials: dict):
|
||||
"""
|
||||
Validates the given credentials.
|
||||
"""
|
||||
if 'api_key' not in credentials:
|
||||
raise CredentialsValidateFailedError('Jina API Key must be provided.')
|
||||
|
||||
try:
|
||||
credential_kwargs = {
|
||||
'api_key': credentials['api_key'],
|
||||
}
|
||||
|
||||
embedding = JinaEmbeddings(
|
||||
model='jina-embeddings-v2-small-en',
|
||||
**credential_kwargs
|
||||
)
|
||||
|
||||
embedding.embed_query("ping")
|
||||
except Exception as ex:
|
||||
raise CredentialsValidateFailedError(str(ex))
|
||||
|
||||
@classmethod
|
||||
def encrypt_provider_credentials(cls, tenant_id: str, credentials: dict) -> dict:
|
||||
credentials['api_key'] = encrypter.encrypt_token(tenant_id, credentials['api_key'])
|
||||
return credentials
|
||||
|
||||
def get_provider_credentials(self, obfuscated: bool = False) -> dict:
|
||||
if self.provider.provider_type == ProviderType.CUSTOM.value:
|
||||
try:
|
||||
credentials = json.loads(self.provider.encrypted_config)
|
||||
except JSONDecodeError:
|
||||
credentials = {
|
||||
'api_key': None,
|
||||
}
|
||||
|
||||
if credentials['api_key']:
|
||||
credentials['api_key'] = encrypter.decrypt_token(
|
||||
self.provider.tenant_id,
|
||||
credentials['api_key']
|
||||
)
|
||||
|
||||
if obfuscated:
|
||||
credentials['api_key'] = encrypter.obfuscated_token(credentials['api_key'])
|
||||
|
||||
return credentials
|
||||
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def is_model_credentials_valid_or_raise(cls, model_name: str, model_type: ModelType, credentials: dict):
|
||||
"""
|
||||
check model credentials valid.
|
||||
|
||||
:param model_name:
|
||||
:param model_type:
|
||||
:param credentials:
|
||||
"""
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def encrypt_model_credentials(cls, tenant_id: str, model_name: str, model_type: ModelType,
|
||||
credentials: dict) -> dict:
|
||||
"""
|
||||
encrypt model credentials for save.
|
||||
|
||||
:param tenant_id:
|
||||
:param model_name:
|
||||
:param model_type:
|
||||
:param credentials:
|
||||
:return:
|
||||
"""
|
||||
return {}
|
||||
|
||||
def get_model_credentials(self, model_name: str, model_type: ModelType, obfuscated: bool = False) -> dict:
|
||||
"""
|
||||
get credentials for llm use.
|
||||
|
||||
:param model_name:
|
||||
:param model_type:
|
||||
:param obfuscated:
|
||||
:return:
|
||||
"""
|
||||
return self.get_provider_credentials(obfuscated)
|
||||
|
||||
def _get_text_generation_model_mode(self, model_name) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_model_parameter_rules(self, model_name: str, model_type: ModelType) -> ModelKwargsRules:
|
||||
raise NotImplementedError
|
||||
@@ -14,5 +14,6 @@
|
||||
"xinference",
|
||||
"openllm",
|
||||
"localai",
|
||||
"cohere"
|
||||
"cohere",
|
||||
"jina"
|
||||
]
|
||||
|
||||
10
api/core/model_providers/rules/jina.json
Normal file
10
api/core/model_providers/rules/jina.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"support_provider_types": [
|
||||
"custom"
|
||||
],
|
||||
"system_config": null,
|
||||
"model_flexibility": "fixed",
|
||||
"supported_model_types": [
|
||||
"embeddings"
|
||||
]
|
||||
}
|
||||
@@ -40,7 +40,7 @@ default_retrieval_model = {
|
||||
'reranking_model_name': ''
|
||||
},
|
||||
'top_k': 2,
|
||||
'score_threshold_enable': False
|
||||
'score_threshold_enabled': False
|
||||
}
|
||||
|
||||
class OrchestratorRuleParser:
|
||||
@@ -220,8 +220,8 @@ class OrchestratorRuleParser:
|
||||
# top_k = self._dynamic_calc_retrieve_k(dataset=dataset, top_k=top_k, rest_tokens=rest_tokens)
|
||||
|
||||
score_threshold = None
|
||||
score_threshold_enable = retrieval_model_config.get("score_threshold_enable")
|
||||
if score_threshold_enable:
|
||||
score_threshold_enabled = retrieval_model_config.get("score_threshold_enabled")
|
||||
if score_threshold_enabled:
|
||||
score_threshold = retrieval_model_config.get("score_threshold")
|
||||
|
||||
tool = DatasetRetrieverTool.from_dataset(
|
||||
@@ -239,7 +239,7 @@ class OrchestratorRuleParser:
|
||||
dataset_ids=dataset_ids,
|
||||
tenant_id=kwargs['tenant_id'],
|
||||
top_k=dataset_configs.get('top_k', 2),
|
||||
score_threshold=dataset_configs.get('score_threshold', 0.5) if dataset_configs.get('score_threshold_enable', False) else None,
|
||||
score_threshold=dataset_configs.get('score_threshold', 0.5) if dataset_configs.get('score_threshold_enabled', False) else None,
|
||||
callbacks=[DatasetToolCallbackHandler(conversation_message_task)],
|
||||
conversation_message_task=conversation_message_task,
|
||||
return_resource=return_resource,
|
||||
|
||||
69
api/core/third_party/langchain/embeddings/jina_embedding.py
vendored
Normal file
69
api/core/third_party/langchain/embeddings/jina_embedding.py
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Wrapper around Jina embedding models."""
|
||||
from typing import Any, List
|
||||
|
||||
import requests
|
||||
from pydantic import BaseModel, Extra
|
||||
|
||||
from langchain.embeddings.base import Embeddings
|
||||
|
||||
|
||||
class JinaEmbeddings(BaseModel, Embeddings):
|
||||
"""Wrapper around Jina embedding models.
|
||||
"""
|
||||
|
||||
client: Any #: :meta private:
|
||||
api_key: str
|
||||
model: str
|
||||
|
||||
class Config:
|
||||
"""Configuration for this pydantic object."""
|
||||
|
||||
extra = Extra.forbid
|
||||
|
||||
def embed_documents(self, texts: List[str]) -> List[List[float]]:
|
||||
"""Call out to Jina's embedding endpoint.
|
||||
|
||||
Args:
|
||||
texts: The list of texts to embed.
|
||||
|
||||
Returns:
|
||||
List of embeddings, one for each text.
|
||||
"""
|
||||
embeddings = []
|
||||
for text in texts:
|
||||
result = self.invoke_embedding(text=text)
|
||||
embeddings.append(result)
|
||||
|
||||
return [list(map(float, e)) for e in embeddings]
|
||||
|
||||
def invoke_embedding(self, text):
|
||||
params = {
|
||||
"model": self.model,
|
||||
"input": [
|
||||
text
|
||||
]
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.api_key}"}
|
||||
response = requests.post(
|
||||
'https://api.jina.ai/v1/embeddings',
|
||||
headers=headers,
|
||||
json=params
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
raise ValueError(f"Jina HTTP {response.status_code} error: {response.text}")
|
||||
|
||||
json_response = response.json()
|
||||
return json_response["data"][0]["embedding"]
|
||||
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
"""Call out to Jina's embedding endpoint.
|
||||
|
||||
Args:
|
||||
text: The text to embed.
|
||||
|
||||
Returns:
|
||||
Embeddings for the text.
|
||||
"""
|
||||
return self.embed_documents([text])[0]
|
||||
@@ -24,7 +24,7 @@ default_retrieval_model = {
|
||||
'reranking_model_name': ''
|
||||
},
|
||||
'top_k': 2,
|
||||
'score_threshold_enable': False
|
||||
'score_threshold_enabled': False
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ class DatasetMultiRetrieverTool(BaseTool):
|
||||
'embeddings': embeddings,
|
||||
'score_threshold': retrieval_model[
|
||||
'score_threshold'] if retrieval_model[
|
||||
'score_threshold_enable'] else None,
|
||||
'score_threshold_enabled'] else None,
|
||||
'top_k': self.top_k,
|
||||
'reranking_model': retrieval_model[
|
||||
'reranking_model'] if retrieval_model[
|
||||
|
||||
@@ -25,7 +25,7 @@ default_retrieval_model = {
|
||||
'reranking_model_name': ''
|
||||
},
|
||||
'top_k': 2,
|
||||
'score_threshold_enable': False
|
||||
'score_threshold_enabled': False
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class DatasetRetrieverTool(BaseTool):
|
||||
'query': query,
|
||||
'top_k': self.top_k,
|
||||
'score_threshold': retrieval_model['score_threshold'] if retrieval_model[
|
||||
'score_threshold_enable'] else None,
|
||||
'score_threshold_enabled'] else None,
|
||||
'reranking_model': retrieval_model['reranking_model'] if retrieval_model[
|
||||
'reranking_enable'] else None,
|
||||
'all_documents': documents,
|
||||
@@ -129,7 +129,7 @@ class DatasetRetrieverTool(BaseTool):
|
||||
'search_method': retrieval_model['search_method'],
|
||||
'embeddings': embeddings,
|
||||
'score_threshold': retrieval_model['score_threshold'] if retrieval_model[
|
||||
'score_threshold_enable'] else None,
|
||||
'score_threshold_enabled'] else None,
|
||||
'top_k': self.top_k,
|
||||
'reranking_model': retrieval_model['reranking_model'] if retrieval_model[
|
||||
'reranking_enable'] else None,
|
||||
@@ -148,7 +148,7 @@ class DatasetRetrieverTool(BaseTool):
|
||||
model_name=retrieval_model['reranking_model']['reranking_model_name']
|
||||
)
|
||||
documents = hybrid_rerank.rerank(query, documents,
|
||||
retrieval_model['score_threshold'] if retrieval_model['score_threshold_enable'] else None,
|
||||
retrieval_model['score_threshold'] if retrieval_model['score_threshold_enabled'] else None,
|
||||
self.top_k)
|
||||
else:
|
||||
documents = []
|
||||
|
||||
@@ -22,7 +22,7 @@ dataset_retrieval_model_fields = {
|
||||
'reranking_enable': fields.Boolean,
|
||||
'reranking_model': fields.Nested(reranking_model_fields),
|
||||
'top_k': fields.Integer,
|
||||
'score_threshold_enable': fields.Boolean,
|
||||
'score_threshold_enabled': fields.Boolean,
|
||||
'score_threshold': fields.Float
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class Dataset(db.Model):
|
||||
'reranking_model_name': ''
|
||||
},
|
||||
'top_k': 2,
|
||||
'score_threshold_enable': False
|
||||
'score_threshold_enabled': False
|
||||
}
|
||||
return self.retrieval_model if self.retrieval_model else default_retrieval_model
|
||||
|
||||
|
||||
54
api/services/billing_service.py
Normal file
54
api/services/billing_service.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
import requests
|
||||
|
||||
|
||||
class BillingService:
|
||||
base_url = os.environ.get('BILLING_API_URL', 'BILLING_API_URL')
|
||||
secret_key = os.environ.get('BILLING_API_SECRET_KEY', 'BILLING_API_SECRET_KEY')
|
||||
|
||||
@classmethod
|
||||
def get_info(cls, tenant_id: str):
|
||||
params = {'tenant_id': tenant_id}
|
||||
|
||||
billing_info = cls._send_request('GET', '/info', params=params)
|
||||
|
||||
return billing_info
|
||||
|
||||
@classmethod
|
||||
def get_subscription(cls, plan: str,
|
||||
interval: str,
|
||||
prefilled_email: str = '',
|
||||
user_name: str = '',
|
||||
tenant_id: str = ''):
|
||||
params = {
|
||||
'plan': plan,
|
||||
'interval': interval,
|
||||
'prefilled_email': prefilled_email,
|
||||
'user_name': user_name,
|
||||
'tenant_id': tenant_id
|
||||
}
|
||||
return cls._send_request('GET', '/subscription', params=params)
|
||||
|
||||
@classmethod
|
||||
def get_invoices(cls, prefilled_email: str = ''):
|
||||
params = {'prefilled_email': prefilled_email}
|
||||
return cls._send_request('GET', '/invoices', params=params)
|
||||
|
||||
@classmethod
|
||||
def _send_request(cls, method, endpoint, json=None, params=None):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Billing-Api-Secret-Key": cls.secret_key
|
||||
}
|
||||
|
||||
url = f"{cls.base_url}{endpoint}"
|
||||
response = requests.request(method, url, json=json, params=params, headers=headers)
|
||||
|
||||
return response.json()
|
||||
|
||||
@classmethod
|
||||
def process_event(cls, event: dict):
|
||||
json = {
|
||||
"content": event,
|
||||
}
|
||||
return cls._send_request('POST', '/webhook/stripe', json=json)
|
||||
@@ -11,7 +11,7 @@ from services.errors.message import MessageNotExistsError
|
||||
|
||||
class ConversationService:
|
||||
@classmethod
|
||||
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
|
||||
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account, EndUser]],
|
||||
last_id: Optional[str], limit: int,
|
||||
include_ids: Optional[list] = None, exclude_ids: Optional[list] = None,
|
||||
exclude_debug_conversation: bool = False) -> InfiniteScrollPagination:
|
||||
@@ -69,7 +69,7 @@ class ConversationService:
|
||||
|
||||
@classmethod
|
||||
def rename(cls, app_model: App, conversation_id: str,
|
||||
user: Optional[Union[Account | EndUser]], name: str, auto_generate: bool):
|
||||
user: Optional[Union[Account, EndUser]], name: str, auto_generate: bool):
|
||||
conversation = cls.get_conversation(app_model, conversation_id, user)
|
||||
|
||||
if auto_generate:
|
||||
@@ -104,7 +104,7 @@ class ConversationService:
|
||||
return conversation
|
||||
|
||||
@classmethod
|
||||
def get_conversation(cls, app_model: App, conversation_id: str, user: Optional[Union[Account | EndUser]]):
|
||||
def get_conversation(cls, app_model: App, conversation_id: str, user: Optional[Union[Account, EndUser]]):
|
||||
conversation = db.session.query(Conversation) \
|
||||
.filter(
|
||||
Conversation.id == conversation_id,
|
||||
@@ -121,7 +121,7 @@ class ConversationService:
|
||||
return conversation
|
||||
|
||||
@classmethod
|
||||
def delete(cls, app_model: App, conversation_id: str, user: Optional[Union[Account | EndUser]]):
|
||||
def delete(cls, app_model: App, conversation_id: str, user: Optional[Union[Account, EndUser]]):
|
||||
conversation = cls.get_conversation(app_model, conversation_id, user)
|
||||
|
||||
conversation.is_deleted = True
|
||||
|
||||
@@ -450,11 +450,6 @@ class DocumentService:
|
||||
notion_info_list = document_data["data_source"]['info_list']['notion_info_list']
|
||||
for notion_info in notion_info_list:
|
||||
count = count + len(notion_info['pages'])
|
||||
documents_count = DocumentService.get_tenant_documents_count()
|
||||
total_count = documents_count + count
|
||||
tenant_document_count = int(current_app.config['TENANT_DOCUMENT_COUNT'])
|
||||
if total_count > tenant_document_count:
|
||||
raise ValueError(f"over document limit {tenant_document_count}.")
|
||||
# if dataset is empty, update dataset data_source_type
|
||||
if not dataset.data_source_type:
|
||||
dataset.data_source_type = document_data["data_source"]["type"]
|
||||
@@ -485,10 +480,11 @@ class DocumentService:
|
||||
'reranking_model_name': ''
|
||||
},
|
||||
'top_k': 2,
|
||||
'score_threshold_enable': False
|
||||
'score_threshold_enabled': False
|
||||
}
|
||||
|
||||
dataset.retrieval_model = document_data.get('retrieval_model') if document_data.get('retrieval_model') else default_retrieval_model
|
||||
dataset.retrieval_model = document_data.get('retrieval_model') if document_data.get(
|
||||
'retrieval_model') else default_retrieval_model
|
||||
|
||||
documents = []
|
||||
batch = time.strftime('%Y%m%d%H%M%S') + str(random.randint(100000, 999999))
|
||||
@@ -739,13 +735,7 @@ class DocumentService:
|
||||
notion_info_list = document_data["data_source"]['info_list']['notion_info_list']
|
||||
for notion_info in notion_info_list:
|
||||
count = count + len(notion_info['pages'])
|
||||
# check document limit
|
||||
if current_app.config['EDITION'] == 'CLOUD':
|
||||
documents_count = DocumentService.get_tenant_documents_count()
|
||||
total_count = documents_count + count
|
||||
tenant_document_count = int(current_app.config['TENANT_DOCUMENT_COUNT'])
|
||||
if total_count > tenant_document_count:
|
||||
raise ValueError(f"All your documents have overed limit {tenant_document_count}.")
|
||||
|
||||
embedding_model = None
|
||||
dataset_collection_binding_id = None
|
||||
retrieval_model = None
|
||||
@@ -769,7 +759,7 @@ class DocumentService:
|
||||
'reranking_model_name': ''
|
||||
},
|
||||
'top_k': 2,
|
||||
'score_threshold_enable': False
|
||||
'score_threshold_enabled': False
|
||||
}
|
||||
retrieval_model = default_retrieval_model
|
||||
# save dataset
|
||||
|
||||
@@ -25,7 +25,7 @@ default_retrieval_model = {
|
||||
'reranking_model_name': ''
|
||||
},
|
||||
'top_k': 2,
|
||||
'score_threshold_enable': False
|
||||
'score_threshold_enabled': False
|
||||
}
|
||||
|
||||
class HitTestingService:
|
||||
@@ -64,7 +64,7 @@ class HitTestingService:
|
||||
'dataset_id': str(dataset.id),
|
||||
'query': query,
|
||||
'top_k': retrieval_model['top_k'],
|
||||
'score_threshold': retrieval_model['score_threshold'] if retrieval_model['score_threshold_enable'] else None,
|
||||
'score_threshold': retrieval_model['score_threshold'] if retrieval_model['score_threshold_enabled'] else None,
|
||||
'reranking_model': retrieval_model['reranking_model'] if retrieval_model['reranking_enable'] else None,
|
||||
'all_documents': all_documents,
|
||||
'search_method': retrieval_model['search_method'],
|
||||
@@ -81,7 +81,7 @@ class HitTestingService:
|
||||
'query': query,
|
||||
'search_method': retrieval_model['search_method'],
|
||||
'embeddings': embeddings,
|
||||
'score_threshold': retrieval_model['score_threshold'] if retrieval_model['score_threshold_enable'] else None,
|
||||
'score_threshold': retrieval_model['score_threshold'] if retrieval_model['score_threshold_enabled'] else None,
|
||||
'top_k': retrieval_model['top_k'],
|
||||
'reranking_model': retrieval_model['reranking_model'] if retrieval_model['reranking_enable'] else None,
|
||||
'all_documents': all_documents
|
||||
@@ -99,7 +99,7 @@ class HitTestingService:
|
||||
model_name=retrieval_model['reranking_model']['reranking_model_name']
|
||||
)
|
||||
all_documents = hybrid_rerank.rerank(query, all_documents,
|
||||
retrieval_model['score_threshold'] if retrieval_model['score_threshold_enable'] else None,
|
||||
retrieval_model['score_threshold'] if retrieval_model['score_threshold_enabled'] else None,
|
||||
retrieval_model['top_k'])
|
||||
|
||||
end = time.perf_counter()
|
||||
|
||||
@@ -16,7 +16,7 @@ from services.errors.message import FirstMessageNotExistsError, MessageNotExists
|
||||
|
||||
class MessageService:
|
||||
@classmethod
|
||||
def pagination_by_first_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
|
||||
def pagination_by_first_id(cls, app_model: App, user: Optional[Union[Account, EndUser]],
|
||||
conversation_id: str, first_id: Optional[str], limit: int) -> InfiniteScrollPagination:
|
||||
if not user:
|
||||
return InfiniteScrollPagination(data=[], limit=limit, has_more=False)
|
||||
@@ -68,7 +68,7 @@ class MessageService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
|
||||
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account, EndUser]],
|
||||
last_id: Optional[str], limit: int, conversation_id: Optional[str] = None,
|
||||
include_ids: Optional[list] = None) -> InfiniteScrollPagination:
|
||||
if not user:
|
||||
@@ -119,7 +119,7 @@ class MessageService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_feedback(cls, app_model: App, message_id: str, user: Optional[Union[Account | EndUser]],
|
||||
def create_feedback(cls, app_model: App, message_id: str, user: Optional[Union[Account, EndUser]],
|
||||
rating: Optional[str]) -> MessageFeedback:
|
||||
if not user:
|
||||
raise ValueError('user cannot be None')
|
||||
@@ -155,7 +155,7 @@ class MessageService:
|
||||
return feedback
|
||||
|
||||
@classmethod
|
||||
def get_message(cls, app_model: App, user: Optional[Union[Account | EndUser]], message_id: str):
|
||||
def get_message(cls, app_model: App, user: Optional[Union[Account, EndUser]], message_id: str):
|
||||
message = db.session.query(Message).filter(
|
||||
Message.id == message_id,
|
||||
Message.app_id == app_model.id,
|
||||
@@ -170,7 +170,7 @@ class MessageService:
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def get_suggested_questions_after_answer(cls, app_model: App, user: Optional[Union[Account | EndUser]],
|
||||
def get_suggested_questions_after_answer(cls, app_model: App, user: Optional[Union[Account, EndUser]],
|
||||
message_id: str, check_enabled: bool = True) -> List[Message]:
|
||||
if not user:
|
||||
raise ValueError('user cannot be None')
|
||||
|
||||
@@ -15,7 +15,7 @@ default_retrieval_model = {
|
||||
'reranking_model_name': ''
|
||||
},
|
||||
'top_k': 2,
|
||||
'score_threshold_enable': False
|
||||
'score_threshold_enabled': False
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from services.message_service import MessageService
|
||||
|
||||
class SavedMessageService:
|
||||
@classmethod
|
||||
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
|
||||
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account, EndUser]],
|
||||
last_id: Optional[str], limit: int) -> InfiniteScrollPagination:
|
||||
saved_messages = db.session.query(SavedMessage).filter(
|
||||
SavedMessage.app_id == app_model.id,
|
||||
@@ -28,7 +28,7 @@ class SavedMessageService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def save(cls, app_model: App, user: Optional[Union[Account | EndUser]], message_id: str):
|
||||
def save(cls, app_model: App, user: Optional[Union[Account, EndUser]], message_id: str):
|
||||
saved_message = db.session.query(SavedMessage).filter(
|
||||
SavedMessage.app_id == app_model.id,
|
||||
SavedMessage.message_id == message_id,
|
||||
@@ -56,7 +56,7 @@ class SavedMessageService:
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def delete(cls, app_model: App, user: Optional[Union[Account | EndUser]], message_id: str):
|
||||
def delete(cls, app_model: App, user: Optional[Union[Account, EndUser]], message_id: str):
|
||||
saved_message = db.session.query(SavedMessage).filter(
|
||||
SavedMessage.app_id == app_model.id,
|
||||
SavedMessage.message_id == message_id,
|
||||
|
||||
@@ -10,7 +10,7 @@ from services.conversation_service import ConversationService
|
||||
|
||||
class WebConversationService:
|
||||
@classmethod
|
||||
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account | EndUser]],
|
||||
def pagination_by_last_id(cls, app_model: App, user: Optional[Union[Account, EndUser]],
|
||||
last_id: Optional[str], limit: int, pinned: Optional[bool] = None,
|
||||
exclude_debug_conversation: bool = False) -> InfiniteScrollPagination:
|
||||
include_ids = None
|
||||
@@ -38,7 +38,7 @@ class WebConversationService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def pin(cls, app_model: App, conversation_id: str, user: Optional[Union[Account | EndUser]]):
|
||||
def pin(cls, app_model: App, conversation_id: str, user: Optional[Union[Account, EndUser]]):
|
||||
pinned_conversation = db.session.query(PinnedConversation).filter(
|
||||
PinnedConversation.app_id == app_model.id,
|
||||
PinnedConversation.conversation_id == conversation_id,
|
||||
@@ -66,7 +66,7 @@ class WebConversationService:
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def unpin(cls, app_model: App, conversation_id: str, user: Optional[Union[Account | EndUser]]):
|
||||
def unpin(cls, app_model: App, conversation_id: str, user: Optional[Union[Account, EndUser]]):
|
||||
pinned_conversation = db.session.query(PinnedConversation).filter(
|
||||
PinnedConversation.app_id == app_model.id,
|
||||
PinnedConversation.conversation_id == conversation_id,
|
||||
|
||||
@@ -53,4 +53,7 @@ OPENLLM_SERVER_URL=
|
||||
LOCALAI_SERVER_URL=
|
||||
|
||||
# Cohere Credentials
|
||||
COHERE_API_KEY=
|
||||
COHERE_API_KEY=
|
||||
|
||||
# Jina Credentials
|
||||
JINA_API_KEY=
|
||||
@@ -0,0 +1,42 @@
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from core.model_providers.models.embedding.jina_embedding import JinaEmbedding
|
||||
from core.model_providers.providers.jina_provider import JinaProvider
|
||||
from models.provider import Provider, ProviderType
|
||||
|
||||
|
||||
def get_mock_provider(valid_api_key):
|
||||
return Provider(
|
||||
id='provider_id',
|
||||
tenant_id='tenant_id',
|
||||
provider_name='jina',
|
||||
provider_type=ProviderType.CUSTOM.value,
|
||||
encrypted_config=json.dumps({
|
||||
'api_key': valid_api_key
|
||||
}),
|
||||
is_valid=True,
|
||||
)
|
||||
|
||||
|
||||
def get_mock_embedding_model():
|
||||
model_name = 'jina-embeddings-v2-small-en'
|
||||
valid_api_key = os.environ['JINA_API_KEY']
|
||||
provider = JinaProvider(provider=get_mock_provider(valid_api_key))
|
||||
return JinaEmbedding(
|
||||
model_provider=provider,
|
||||
name=model_name
|
||||
)
|
||||
|
||||
|
||||
def decrypt_side_effect(tenant_id, encrypted_api_key):
|
||||
return encrypted_api_key
|
||||
|
||||
|
||||
@patch('core.helper.encrypter.decrypt_token', side_effect=decrypt_side_effect)
|
||||
def test_embedding(mock_decrypt):
|
||||
embedding_model = get_mock_embedding_model()
|
||||
rst = embedding_model.client.embed_query('test')
|
||||
assert isinstance(rst, list)
|
||||
assert len(rst) == 512
|
||||
88
api/tests/unit_tests/model_providers/test_jina_provider.py
Normal file
88
api/tests/unit_tests/model_providers/test_jina_provider.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
import json
|
||||
|
||||
from core.model_providers.providers.base import CredentialsValidateFailedError
|
||||
from core.model_providers.providers.jina_provider import JinaProvider
|
||||
from models.provider import ProviderType, Provider
|
||||
|
||||
|
||||
PROVIDER_NAME = 'jina'
|
||||
MODEL_PROVIDER_CLASS = JinaProvider
|
||||
VALIDATE_CREDENTIAL = {
|
||||
'api_key': 'valid_key'
|
||||
}
|
||||
|
||||
|
||||
def encrypt_side_effect(tenant_id, encrypt_key):
|
||||
return f'encrypted_{encrypt_key}'
|
||||
|
||||
|
||||
def decrypt_side_effect(tenant_id, encrypted_key):
|
||||
return encrypted_key.replace('encrypted_', '')
|
||||
|
||||
|
||||
def test_is_provider_credentials_valid_or_raise_valid(mocker):
|
||||
mocker.patch('core.third_party.langchain.embeddings.jina_embedding.JinaEmbeddings.embed_query',
|
||||
return_value=[1, 2])
|
||||
|
||||
MODEL_PROVIDER_CLASS.is_provider_credentials_valid_or_raise(VALIDATE_CREDENTIAL)
|
||||
|
||||
|
||||
def test_is_provider_credentials_valid_or_raise_invalid():
|
||||
# raise CredentialsValidateFailedError if api_key is not in credentials
|
||||
with pytest.raises(CredentialsValidateFailedError):
|
||||
MODEL_PROVIDER_CLASS.is_provider_credentials_valid_or_raise({})
|
||||
|
||||
credential = VALIDATE_CREDENTIAL.copy()
|
||||
credential['api_key'] = 'invalid_key'
|
||||
|
||||
# raise CredentialsValidateFailedError if api_key is invalid
|
||||
with pytest.raises(CredentialsValidateFailedError):
|
||||
MODEL_PROVIDER_CLASS.is_provider_credentials_valid_or_raise(credential)
|
||||
|
||||
|
||||
@patch('core.helper.encrypter.encrypt_token', side_effect=encrypt_side_effect)
|
||||
def test_encrypt_credentials(mock_encrypt):
|
||||
api_key = 'valid_key'
|
||||
result = MODEL_PROVIDER_CLASS.encrypt_provider_credentials('tenant_id', VALIDATE_CREDENTIAL.copy())
|
||||
mock_encrypt.assert_called_with('tenant_id', api_key)
|
||||
assert result['api_key'] == f'encrypted_{api_key}'
|
||||
|
||||
|
||||
@patch('core.helper.encrypter.decrypt_token', side_effect=decrypt_side_effect)
|
||||
def test_get_credentials_custom(mock_decrypt):
|
||||
encrypted_credential = VALIDATE_CREDENTIAL.copy()
|
||||
encrypted_credential['api_key'] = 'encrypted_' + encrypted_credential['api_key']
|
||||
|
||||
provider = Provider(
|
||||
id='provider_id',
|
||||
tenant_id='tenant_id',
|
||||
provider_name=PROVIDER_NAME,
|
||||
provider_type=ProviderType.CUSTOM.value,
|
||||
encrypted_config=json.dumps(encrypted_credential),
|
||||
is_valid=True,
|
||||
)
|
||||
model_provider = MODEL_PROVIDER_CLASS(provider=provider)
|
||||
result = model_provider.get_provider_credentials()
|
||||
assert result['api_key'] == 'valid_key'
|
||||
|
||||
|
||||
@patch('core.helper.encrypter.decrypt_token', side_effect=decrypt_side_effect)
|
||||
def test_get_credentials_obfuscated(mock_decrypt):
|
||||
encrypted_credential = VALIDATE_CREDENTIAL.copy()
|
||||
encrypted_credential['api_key'] = 'encrypted_' + encrypted_credential['api_key']
|
||||
|
||||
provider = Provider(
|
||||
id='provider_id',
|
||||
tenant_id='tenant_id',
|
||||
provider_name=PROVIDER_NAME,
|
||||
provider_type=ProviderType.CUSTOM.value,
|
||||
encrypted_config=json.dumps(encrypted_credential),
|
||||
is_valid=True,
|
||||
)
|
||||
model_provider = MODEL_PROVIDER_CLASS(provider=provider)
|
||||
result = model_provider.get_provider_credentials(obfuscated=True)
|
||||
middle_token = result['api_key'][6:-2]
|
||||
assert len(middle_token) == max(len(VALIDATE_CREDENTIAL['api_key']) - 8, 0)
|
||||
assert all(char == '*' for char in middle_token)
|
||||
@@ -52,15 +52,15 @@ services:
|
||||
- "8080:8080"
|
||||
|
||||
# Qdrant vector store.
|
||||
# uncomment to use qdrant as vector store.
|
||||
# (if uncommented, you need to comment out the weaviate service above,
|
||||
# and set VECTOR_STORE to qdrant in the api & worker service.)
|
||||
# qdrant:
|
||||
# image: qdrant/qdrant:latest
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - ./volumes/qdrant:/qdrant/storage
|
||||
# environment:
|
||||
# QDRANT__API_KEY: 'difyai123456'
|
||||
# ports:
|
||||
# - "6333:6333"
|
||||
# uncomment to use qdrant as vector store.
|
||||
# (if uncommented, you need to comment out the weaviate service above,
|
||||
# and set VECTOR_STORE to qdrant in the api & worker service.)
|
||||
# qdrant:
|
||||
# image: qdrant/qdrant:latest
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - ./volumes/qdrant:/qdrant/storage
|
||||
# environment:
|
||||
# QDRANT__API_KEY: 'difyai123456'
|
||||
# ports:
|
||||
# - "6333:6333"
|
||||
|
||||
@@ -2,7 +2,7 @@ version: '3.1'
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.3.32
|
||||
image: langgenius/dify-api:0.3.33
|
||||
restart: always
|
||||
environment:
|
||||
# Startup mode, 'api' starts the API server.
|
||||
@@ -121,14 +121,14 @@ services:
|
||||
volumes:
|
||||
# Mount the storage directory to the container, for storing user files.
|
||||
- ./volumes/app/storage:/app/api/storage
|
||||
# uncomment to expose dify-api port to host
|
||||
# ports:
|
||||
# - "5001:5001"
|
||||
# uncomment to expose dify-api port to host
|
||||
# ports:
|
||||
# - "5001:5001"
|
||||
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.3.32
|
||||
image: langgenius/dify-api:0.3.33
|
||||
restart: always
|
||||
environment:
|
||||
# Startup mode, 'worker' starts the Celery worker for processing the queue.
|
||||
@@ -196,7 +196,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.3.32
|
||||
image: langgenius/dify-web:0.3.33
|
||||
restart: always
|
||||
environment:
|
||||
EDITION: SELF_HOSTED
|
||||
@@ -210,9 +210,9 @@ services:
|
||||
APP_API_URL: ''
|
||||
# The DSN for Sentry error reporting. If not set, Sentry error reporting will be disabled.
|
||||
SENTRY_DSN: ''
|
||||
# uncomment to expose dify-web port to host
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
# uncomment to expose dify-web port to host
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
|
||||
# The postgres database.
|
||||
db:
|
||||
@@ -247,9 +247,9 @@ services:
|
||||
command: redis-server --requirepass difyai123456
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli","ping"]
|
||||
# uncomment to expose redis port to host
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
# uncomment to expose redis port to host
|
||||
# ports:
|
||||
# - "6379:6379"
|
||||
|
||||
# The Weaviate vector store.
|
||||
weaviate:
|
||||
@@ -271,24 +271,24 @@ services:
|
||||
AUTHENTICATION_APIKEY_USERS: 'hello@dify.ai'
|
||||
AUTHORIZATION_ADMINLIST_ENABLED: 'true'
|
||||
AUTHORIZATION_ADMINLIST_USERS: 'hello@dify.ai'
|
||||
# uncomment to expose weaviate port to host
|
||||
# ports:
|
||||
# - "8080:8080"
|
||||
# uncomment to expose weaviate port to host
|
||||
# ports:
|
||||
# - "8080:8080"
|
||||
|
||||
# Qdrant vector store.
|
||||
# uncomment to use qdrant as vector store.
|
||||
# (if uncommented, you need to comment out the weaviate service above,
|
||||
# and set VECTOR_STORE to qdrant in the api & worker service.)
|
||||
# qdrant:
|
||||
# image: langgenius/qdrant:latest
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - ./volumes/qdrant:/qdrant/storage
|
||||
# environment:
|
||||
# QDRANT__API_KEY: 'difyai123456'
|
||||
## uncomment to expose qdrant port to host
|
||||
## ports:
|
||||
## - "6333:6333"
|
||||
# uncomment to use qdrant as vector store.
|
||||
# (if uncommented, you need to comment out the weaviate service above,
|
||||
# and set VECTOR_STORE to qdrant in the api & worker service.)
|
||||
# qdrant:
|
||||
# image: langgenius/qdrant:latest
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - ./volumes/qdrant:/qdrant/storage
|
||||
# environment:
|
||||
# QDRANT__API_KEY: 'difyai123456'
|
||||
# # uncomment to expose qdrant port to host
|
||||
# # ports:
|
||||
# # - "6333:6333"
|
||||
|
||||
# The nginx reverse proxy.
|
||||
# used for reverse proxying the API service and Web service.
|
||||
|
||||
@@ -44,23 +44,6 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the
|
||||
|
||||
You can start editing the file under folder `app`. The page auto-updates as you edit the file.
|
||||
|
||||
### Run by Docker
|
||||
First, Build the frontend image:
|
||||
```bash
|
||||
docker build . -t dify-web
|
||||
```
|
||||
|
||||
Then, configure the environment variables.Use the same method mentioned in run by source code.
|
||||
|
||||
Finally, run the frontend service:
|
||||
```bash
|
||||
docker run -it -p 3000:3000 -e EDITION=SELF_HOSTED -e CONSOLE_URL=http://127.0.0.1:3000 -e APP_URL=http://127.0.0.1:3000 dify-web
|
||||
```
|
||||
|
||||
When the console api domain and web app api domain are different, you can set the CONSOLE_URL and APP_URL separately.
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## Deploy
|
||||
### Deploy on server
|
||||
First, build the app for production:
|
||||
|
||||
@@ -39,10 +39,10 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const navigation = useMemo(() => {
|
||||
const navs = [
|
||||
{ name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon },
|
||||
isCurrentWorkspaceManager ? { name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon } : false,
|
||||
...(isCurrentWorkspaceManager ? [{ name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon }] : []),
|
||||
{ name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||
{ name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
|
||||
].filter(nav => !!nav)
|
||||
]
|
||||
return navs
|
||||
}, [appId, isCurrentWorkspaceManager, t])
|
||||
|
||||
@@ -56,7 +56,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
return (
|
||||
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
|
||||
<AppSideBar title={response.name} icon={response.icon} icon_background={response.icon_background} desc={appModeName} navigation={navigation} />
|
||||
<div className="bg-white grow">{children}</div>
|
||||
<div className="bg-white grow overflow-hidden">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-max grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
|
||||
<div className="grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
|
||||
<AppCard
|
||||
appInfo={response}
|
||||
cardType="webapp"
|
||||
|
||||
@@ -19,7 +19,7 @@ const Overview = async ({
|
||||
*/
|
||||
const { t } = await translate(locale, 'app-overview')
|
||||
return (
|
||||
<div className="h-full px-16 py-6 overflow-scroll">
|
||||
<div className="h-full px-4 sm:px-16 py-6 overflow-scroll">
|
||||
<ApikeyInfoPanel />
|
||||
<div className='flex flex-row items-center justify-between mb-4 text-xl text-gray-900'>
|
||||
{t('overview.title')}
|
||||
|
||||
@@ -138,6 +138,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
<>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
if (showSettingsModal)
|
||||
return
|
||||
e.preventDefault()
|
||||
push(`/app/${app.id}/overview`)
|
||||
}}
|
||||
|
||||
@@ -10,7 +10,6 @@ import { fetchAppList } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { CheckModal } from '@/hooks/use-pay'
|
||||
|
||||
const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
|
||||
if (!pageIndex || previousPageData.has_more)
|
||||
return { url: 'apps', params: { page: pageIndex + 1, limit: 30 } }
|
||||
|
||||
@@ -16,8 +16,9 @@ import { ToastContext } from '@/app/components/base/toast'
|
||||
import { createApp, fetchAppTemplates } from '@/service/apps'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext from '@/context/app-context'
|
||||
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
|
||||
type NewAppDialogProps = {
|
||||
show: boolean
|
||||
@@ -54,6 +55,9 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
}
|
||||
}, [mutateTemplates, show])
|
||||
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
const onCreate: MouseEventHandler = useCallback(async () => {
|
||||
const name = nameInputRef.current?.value
|
||||
@@ -111,7 +115,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||
<Button disabled={isAppsFull} type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
@@ -122,7 +126,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' placeholder={t('app.appNamePlaceholder') || ''}/>
|
||||
</div>
|
||||
|
||||
<div className='h-[247px] overflow-y-auto'>
|
||||
<div className='overflow-y-auto'>
|
||||
<div className={style.newItemCaption}>
|
||||
<h3 className='inline'>{t('app.newApp.captionAppType')}</h3>
|
||||
{isWithTemplate && (
|
||||
@@ -139,7 +143,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
</div>
|
||||
{isWithTemplate
|
||||
? (
|
||||
<ul className='grid grid-cols-2 gap-4'>
|
||||
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
{templates?.data?.map((template, index) => (
|
||||
<li
|
||||
key={index}
|
||||
@@ -161,7 +165,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<ul className='grid grid-cols-2 gap-4'>
|
||||
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
<li
|
||||
className={classNames(style.listItem, style.selectable, newAppMode === 'chat' && style.selected)}
|
||||
onClick={() => setNewAppMode('chat')}
|
||||
@@ -208,6 +212,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isAppsFull && <AppsFull loc='app-create' />}
|
||||
</Dialog>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import React, { useEffect } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from 'classnames'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
Cog8ToothIcon,
|
||||
// CommandLineIcon,
|
||||
@@ -11,6 +13,8 @@ import {
|
||||
// eslint-disable-next-line sort-imports
|
||||
PuzzlePieceIcon,
|
||||
DocumentTextIcon,
|
||||
PaperClipIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import {
|
||||
Cog8ToothIcon as Cog8ToothSolidIcon,
|
||||
@@ -20,29 +24,39 @@ import {
|
||||
import Link from 'next/link'
|
||||
import s from './style.module.css'
|
||||
import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
|
||||
import type { RelatedApp } from '@/models/datasets'
|
||||
import type { RelatedApp, RelatedAppResponse } from '@/models/datasets'
|
||||
import { getLocaleOnClient } from '@/i18n/client'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import FloatPopoverContainer from '@/app/components/base/float-popover-container'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const LikedItem: FC<{ type?: 'plugin' | 'app'; appStatus?: boolean; detail: RelatedApp }> = ({
|
||||
type ILikedItemProps = {
|
||||
type?: 'plugin' | 'app'
|
||||
appStatus?: boolean
|
||||
detail: RelatedApp
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
const LikedItem = ({
|
||||
type = 'app',
|
||||
appStatus = true,
|
||||
detail,
|
||||
}) => {
|
||||
isMobile,
|
||||
}: ILikedItemProps) => {
|
||||
return (
|
||||
<Link className={s.itemWrapper} href={`/app/${detail?.id}/overview`}>
|
||||
<div className={s.iconWrapper}>
|
||||
<Link className={classNames(s.itemWrapper, 'px-0 sm:px-3 justify-center sm:justify-start')} href={`/app/${detail?.id}/overview`}>
|
||||
<div className={classNames(s.iconWrapper, 'mr-0 sm:mr-2')}>
|
||||
<AppIcon size='tiny' icon={detail?.icon} background={detail?.icon_background}/>
|
||||
{type === 'app' && (
|
||||
<div className={s.statusPoint}>
|
||||
@@ -50,7 +64,7 @@ const LikedItem: FC<{ type?: 'plugin' | 'app'; appStatus?: boolean; detail: Rela
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={s.appInfo}>{detail?.name || '--'}</div>
|
||||
{!isMobile && <div className={s.appInfo}>{detail?.name || '--'}</div>}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -83,6 +97,68 @@ const BookOpenIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
</svg>
|
||||
}
|
||||
|
||||
type IExtraInfoProps = {
|
||||
isMobile: boolean
|
||||
relatedApps?: RelatedAppResponse
|
||||
}
|
||||
|
||||
const ExtraInfo = ({ isMobile, relatedApps }: IExtraInfoProps) => {
|
||||
const locale = getLocaleOnClient()
|
||||
const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile)
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
setShowTips(!isMobile)
|
||||
}, [isMobile, setShowTips])
|
||||
|
||||
return <div className='w-full flex flex-col items-center'>
|
||||
<Divider className='mt-5' />
|
||||
{(relatedApps?.data && relatedApps?.data?.length > 0) && (
|
||||
<>
|
||||
{!isMobile && <div className={s.subTitle}>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>}
|
||||
{isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
|
||||
{relatedApps?.total || '--'}
|
||||
<PaperClipIcon className='h-4 w-4 text-gray-700' />
|
||||
</div>}
|
||||
{relatedApps?.data?.map((item, index) => (<LikedItem key={index} isMobile={isMobile} detail={item} />))}
|
||||
</>
|
||||
)}
|
||||
{!relatedApps?.data?.length && (
|
||||
<FloatPopoverContainer
|
||||
placement='bottom-start'
|
||||
open={isShowTips}
|
||||
toggle={toggleTips}
|
||||
isMobile={isMobile}
|
||||
triggerElement={
|
||||
<div className={classNames('h-7 w-7 inline-flex justify-center items-center rounded-lg bg-transparent', isShowTips && '!bg-gray-50')}>
|
||||
<QuestionMarkCircleIcon className='h-4 w-4 flex-shrink-0 text-gray-500' />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={classNames('mt-5 p-3', isMobile && 'border-[0.5px] border-gray-200 shadow-lg rounded-lg bg-white w-[150px]')}>
|
||||
<div className='flex items-center justify-start gap-2'>
|
||||
<div className={s.emptyIconDiv}>
|
||||
<Squares2X2Icon className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
<div className={s.emptyIconDiv}>
|
||||
<PuzzlePieceIcon className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 mt-2'>{t('common.datasetMenus.emptyTip')}</div>
|
||||
<a
|
||||
className='inline-flex items-center text-xs text-primary-600 mt-2 cursor-pointer'
|
||||
href={`https://docs.dify.ai/${locale === 'zh-Hans' ? 'v/zh-hans' : ''}/application/prompt-engineering`}
|
||||
target='_blank'
|
||||
>
|
||||
<BookOpenIcon className='mr-1' />
|
||||
{t('common.datasetMenus.viewDoc')}
|
||||
</a>
|
||||
</div>
|
||||
</FloatPopoverContainer>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
@@ -91,6 +167,10 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const pathname = usePathname()
|
||||
const hideSideBar = /documents\/create$/.test(pathname)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const { data: datasetRes, error, mutate: mutateDatasetRes } = useSWR({
|
||||
url: 'fetchDatasetDetail',
|
||||
datasetId,
|
||||
@@ -113,54 +193,18 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
document.title = `${datasetRes.name || 'Dataset'} - Dify`
|
||||
}, [datasetRes])
|
||||
|
||||
const ExtraInfo: FC = () => {
|
||||
const locale = getLocaleOnClient()
|
||||
|
||||
return <div className='w-full'>
|
||||
<Divider className='mt-5' />
|
||||
{relatedApps?.data?.length
|
||||
? (
|
||||
<>
|
||||
<div className={s.subTitle}>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>
|
||||
{relatedApps?.data?.map((item, index) => (<LikedItem key={index} detail={item} />))}
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<div className='mt-5 p-3'>
|
||||
<div className='flex items-center justify-start gap-2'>
|
||||
<div className={s.emptyIconDiv}>
|
||||
<Squares2X2Icon className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
<div className={s.emptyIconDiv}>
|
||||
<PuzzlePieceIcon className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 mt-2'>{t('common.datasetMenus.emptyTip')}</div>
|
||||
<a
|
||||
className='inline-flex items-center text-xs text-primary-600 mt-2 cursor-pointer'
|
||||
href={`https://docs.dify.ai/${locale === 'zh-Hans' ? 'v/zh-hans' : ''}/application/prompt-engineering`}
|
||||
target='_blank'
|
||||
>
|
||||
<BookOpenIcon className='mr-1' />
|
||||
{t('common.datasetMenus.viewDoc')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
if (!datasetRes && !error)
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
<div className='flex'>
|
||||
<div className='flex overflow-hidden'>
|
||||
{!hideSideBar && <AppSideBar
|
||||
title={datasetRes?.name || '--'}
|
||||
icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
|
||||
icon_background={datasetRes?.icon_background || '#F5F5F5'}
|
||||
desc={datasetRes?.description || '--'}
|
||||
navigation={navigation}
|
||||
extraInfo={<ExtraInfo />}
|
||||
extraInfo={<ExtraInfo isMobile={isMobile} relatedApps={relatedApps} />}
|
||||
iconType={datasetRes?.data_source_type === DataSourceType.NOTION ? 'notion' : 'dataset'}
|
||||
/>}
|
||||
<DatasetDetailContext.Provider value={{
|
||||
@@ -168,7 +212,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
dataset: datasetRes,
|
||||
mutateDatasetRes: () => mutateDatasetRes(),
|
||||
}}>
|
||||
<div className="bg-white grow" style={{ minHeight: 'calc(100vh - 56px)' }}>{children}</div>
|
||||
<div className="bg-white grow overflow-hidden">{children}</div>
|
||||
</DatasetDetailContext.Provider>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -11,17 +11,16 @@ const Settings = async ({
|
||||
params: { datasetId },
|
||||
}: Props) => {
|
||||
const locale = getLocaleOnServer()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const { t } = await useTranslation(locale, 'dataset-settings')
|
||||
|
||||
return (
|
||||
<div className='bg-white h-full'>
|
||||
<div className='bg-white h-full overflow-y-auto'>
|
||||
<div className='px-6 py-3'>
|
||||
<div className='mb-1 text-lg font-semibold text-gray-900'>{t('title')}</div>
|
||||
<div className='text-sm text-gray-500'>{t('desc')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Form datasetId={datasetId} />
|
||||
</div>
|
||||
<Form datasetId={datasetId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.itemWrapper {
|
||||
@apply flex items-center w-full h-10 px-3 rounded-lg hover:bg-gray-50 cursor-pointer;
|
||||
@apply flex items-center w-full h-10 rounded-lg hover:bg-gray-50 cursor-pointer;
|
||||
}
|
||||
.appInfo {
|
||||
@apply truncate text-gray-700 text-sm font-normal;
|
||||
}
|
||||
.iconWrapper {
|
||||
@apply relative w-6 h-6 mr-2 bg-[#D5F5F6] rounded-md;
|
||||
@apply relative w-6 h-6 bg-[#D5F5F6] rounded-md;
|
||||
}
|
||||
.statusPoint {
|
||||
@apply flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded;
|
||||
|
||||
@@ -15,10 +15,10 @@ const ApiServer: FC<ApiServerProps> = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-center mr-2 pl-1.5 pr-1 h-8 bg-white/80 border-[0.5px] border-white rounded-lg'>
|
||||
<div className='mr-0.5 px-1.5 h-5 border border-gray-200 text-[11px] text-gray-500 rounded-md'>{t('appApi.apiServer')}</div>
|
||||
<div className='px-1 w-[248px] text-[13px] font-medium text-gray-800'>{apiBaseUrl}</div>
|
||||
<div className='flex items-center flex-wrap gap-y-2'>
|
||||
<div className='flex items-center mr-2 pl-1.5 pr-1 h-8 bg-white/80 border-[0.5px] border-white rounded-lg leading-5'>
|
||||
<div className='mr-0.5 px-1.5 h-5 border border-gray-200 text-[11px] text-gray-500 rounded-md shrink-0'>{t('appApi.apiServer')}</div>
|
||||
<div className='px-1 truncate w-fit sm:w-[248px] text-[13px] font-medium text-gray-800'>{apiBaseUrl}</div>
|
||||
<div className='mx-1 w-[1px] h-[14px] bg-gray-200'></div>
|
||||
<CopyFeedback
|
||||
content={apiBaseUrl}
|
||||
|
||||
@@ -29,7 +29,7 @@ const Container = () => {
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
|
||||
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 h-14 bg-gray-100 z-10'>
|
||||
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
|
||||
<TabSlider
|
||||
value={activeTab}
|
||||
onChange={newActiveTab => setActiveTab(newActiveTab)}
|
||||
@@ -38,16 +38,14 @@ const Container = () => {
|
||||
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
|
||||
</div>
|
||||
|
||||
{activeTab === 'dataset'
|
||||
? (
|
||||
<>
|
||||
<Datasets containerRef={containerRef} />
|
||||
<DatasetFooter />
|
||||
</>
|
||||
)
|
||||
: (
|
||||
activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />
|
||||
)}
|
||||
{activeTab === 'dataset' && (
|
||||
<>
|
||||
<Datasets containerRef={containerRef} />
|
||||
<DatasetFooter />
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />}
|
||||
</div>
|
||||
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ const Doc: FC<DocProps> = ({
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
return (
|
||||
<article className='mx-12 pt-16 bg-white rounded-t-xl prose prose-xl'>
|
||||
<article className='mx-1 px-4 sm:mx-12 pt-16 bg-white rounded-t-xl prose prose-xl'>
|
||||
{
|
||||
locale === 'en'
|
||||
? <TemplateEn apiBaseUrl={apiBaseUrl} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CodeGroup } from '@/app/components/develop/code.tsx'
|
||||
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '@/app/components/develop/md.tsx'
|
||||
|
||||
# Dataset API
|
||||
# Knowledge API
|
||||
|
||||
<div>
|
||||
### Authentication
|
||||
@@ -30,12 +30,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
This api is based on an existing dataset and creates a new document through text based on this dataset.
|
||||
This api is based on an existing Knowledge and creates a new document through text based on this Knowledge.
|
||||
|
||||
### Params
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
@@ -133,12 +133,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
This api is based on an existing dataset and creates a new document through a file based on this dataset.
|
||||
This api is based on an existing Knowledge and creates a new document through a file based on this Knowledge.
|
||||
|
||||
### Params
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
@@ -228,7 +228,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Heading
|
||||
url='/datasets'
|
||||
method='POST'
|
||||
title='Create an empty dataset'
|
||||
title='Create an empty Knowledge'
|
||||
name='#create_empty_dataset'
|
||||
/>
|
||||
<Row>
|
||||
@@ -236,7 +236,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Request Body
|
||||
<Properties>
|
||||
<Property name='name' type='string' key='name'>
|
||||
Dataset name
|
||||
Knowledge name
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
@@ -287,7 +287,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Heading
|
||||
url='/datasets'
|
||||
method='GET'
|
||||
title='Dataset list'
|
||||
title='Knowledge list'
|
||||
name='#dataset_list'
|
||||
/>
|
||||
<Row>
|
||||
@@ -355,12 +355,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
This api is based on an existing dataset and updates the document through text based on this dataset.
|
||||
This api is based on an existing Knowledge and updates the document through text based on this Knowledge.
|
||||
|
||||
### Params
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
Document ID
|
||||
@@ -452,12 +452,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
This api is based on an existing dataset, and updates documents through files based on this dataset
|
||||
This api is based on an existing Knowledge, and updates documents through files based on this Knowledge
|
||||
|
||||
### Params
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
Document ID
|
||||
@@ -549,7 +549,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Params
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
<Property name='batch' type='string' key='batch'>
|
||||
Batch number of uploaded documents
|
||||
@@ -604,7 +604,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Params
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
Document ID
|
||||
@@ -638,7 +638,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Heading
|
||||
url='/datasets/{dataset_id}/documents'
|
||||
method='GET'
|
||||
title='Dataset document list'
|
||||
title='Knowledge document list'
|
||||
name='#dataset_document_list'
|
||||
/>
|
||||
<Row>
|
||||
@@ -646,7 +646,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Params
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
@@ -721,7 +721,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Params
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
Document ID
|
||||
@@ -732,7 +732,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Properties>
|
||||
<Property name='segments' type='object list' key='segments'>
|
||||
- <code>content</code> (text) Text content/question content, required
|
||||
- <code>answer</code> (text) Answer content, if the mode of the data set is qa mode, pass the value(optional)
|
||||
- <code>answer</code> (text) Answer content, if the mode of the Knowledge is qa mode, pass the value(optional)
|
||||
- <code>keywords</code> (list) Keywords(optional)
|
||||
</Property>
|
||||
</Properties>
|
||||
@@ -807,7 +807,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
Document ID
|
||||
@@ -885,7 +885,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
<Property name='segment_id' type='string' key='segment_id'>
|
||||
Document Segment ID
|
||||
@@ -928,7 +928,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### POST
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
Dataset ID
|
||||
Knowledge ID
|
||||
</Property>
|
||||
<Property name='segment_id' type='string' key='segment_id'>
|
||||
Document Segment ID
|
||||
@@ -939,7 +939,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Properties>
|
||||
<Property name='segments' type='object list' key='segments'>
|
||||
- <code>content</code> (text) text content/question content,required
|
||||
- <code>answer</code> (text) Answer content, not required, passed if the data set is in qa mode
|
||||
- <code>answer</code> (text) Answer content, not required, passed if the Knowledge is in qa mode
|
||||
- <code>keywords</code> (list) keyword, not required
|
||||
- <code>enabled</code> (bool) false/true, not required
|
||||
</Property>
|
||||
@@ -1036,72 +1036,72 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<table className="max-w-auto border-collapse border border-slate-400" style={{ maxWidth: 'none', width: 'auto' }}>
|
||||
<thead style={{ background: '#f9fafc' }}>
|
||||
<tr>
|
||||
<th class="p-2 border border-slate-300">code</th>
|
||||
<th class="p-2 border border-slate-300">status</th>
|
||||
<th class="p-2 border border-slate-300">message</th>
|
||||
<th className="p-2 border border-slate-300">code</th>
|
||||
<th className="p-2 border border-slate-300">status</th>
|
||||
<th className="p-2 border border-slate-300">message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">no_file_uploaded</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">Please upload your file.</td>
|
||||
<td className="p-2 border border-slate-300">no_file_uploaded</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">Please upload your file.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">too_many_files</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">Only one file is allowed.</td>
|
||||
<td className="p-2 border border-slate-300">too_many_files</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">Only one file is allowed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">file_too_large</td>
|
||||
<td class="p-2 border border-slate-300">413</td>
|
||||
<td class="p-2 border border-slate-300">File size exceeded.</td>
|
||||
<td className="p-2 border border-slate-300">file_too_large</td>
|
||||
<td className="p-2 border border-slate-300">413</td>
|
||||
<td className="p-2 border border-slate-300">File size exceeded.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">unsupported_file_type</td>
|
||||
<td class="p-2 border border-slate-300">415</td>
|
||||
<td class="p-2 border border-slate-300">File type not allowed.</td>
|
||||
<td className="p-2 border border-slate-300">unsupported_file_type</td>
|
||||
<td className="p-2 border border-slate-300">415</td>
|
||||
<td className="p-2 border border-slate-300">File type not allowed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">high_quality_dataset_only</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">Current operation only supports 'high-quality' datasets.</td>
|
||||
<td className="p-2 border border-slate-300">high_quality_dataset_only</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">Current operation only supports 'high-quality' datasets.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">dataset_not_initialized</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">The dataset is still being initialized or indexing. Please wait a moment.</td>
|
||||
<td className="p-2 border border-slate-300">dataset_not_initialized</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">The dataset is still being initialized or indexing. Please wait a moment.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">archived_document_immutable</td>
|
||||
<td class="p-2 border border-slate-300">403</td>
|
||||
<td class="p-2 border border-slate-300">The archived document is not editable.</td>
|
||||
<td className="p-2 border border-slate-300">archived_document_immutable</td>
|
||||
<td className="p-2 border border-slate-300">403</td>
|
||||
<td className="p-2 border border-slate-300">The archived document is not editable.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">dataset_name_duplicate</td>
|
||||
<td class="p-2 border border-slate-300">409</td>
|
||||
<td class="p-2 border border-slate-300">The dataset name already exists. Please modify your dataset name.</td>
|
||||
<td className="p-2 border border-slate-300">dataset_name_duplicate</td>
|
||||
<td className="p-2 border border-slate-300">409</td>
|
||||
<td className="p-2 border border-slate-300">The dataset name already exists. Please modify your dataset name.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">invalid_action</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">Invalid action.</td>
|
||||
<td className="p-2 border border-slate-300">invalid_action</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">Invalid action.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">document_already_finished</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">The document has been processed. Please refresh the page or go to the document details.</td>
|
||||
<td className="p-2 border border-slate-300">document_already_finished</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">The document has been processed. Please refresh the page or go to the document details.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">document_indexing</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">The document is being processed and cannot be edited.</td>
|
||||
<td className="p-2 border border-slate-300">document_indexing</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">The document is being processed and cannot be edited.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">invalid_metadata</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">The metadata content is incorrect. Please check and verify.</td>
|
||||
<td className="p-2 border border-slate-300">invalid_metadata</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">The metadata content is incorrect. Please check and verify.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pb-4" />
|
||||
<div className="pb-4" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CodeGroup } from '@/app/components/develop/code.tsx'
|
||||
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '@/app/components/develop/md.tsx'
|
||||
|
||||
# 数据集 API
|
||||
# 知识库 API
|
||||
|
||||
<div>
|
||||
### 鉴权
|
||||
@@ -30,12 +30,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
此接口基于已存在数据集,在此数据集的基础上通过文本创建新的文档
|
||||
此接口基于已存在知识库,在此知识库的基础上通过文本创建新的文档
|
||||
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
@@ -133,12 +133,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
此接口基于已存在数据集,在此数据集的基础上通过文件创建新的文档
|
||||
此接口基于已存在知识库,在此知识库的基础上通过文件创建新的文档
|
||||
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
@@ -228,7 +228,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Heading
|
||||
url='/datasets'
|
||||
method='POST'
|
||||
title='创建空数据集'
|
||||
title='创建空知识库'
|
||||
name='#create_empty_dataset'
|
||||
/>
|
||||
<Row>
|
||||
@@ -236,7 +236,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Request Body
|
||||
<Properties>
|
||||
<Property name='name' type='string' key='name'>
|
||||
数据集名称
|
||||
知识库名称
|
||||
</Property>
|
||||
</Properties>
|
||||
</Col>
|
||||
@@ -287,7 +287,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Heading
|
||||
url='/datasets'
|
||||
method='GET'
|
||||
title='数据集列表'
|
||||
title='知识库列表'
|
||||
name='#dataset_list'
|
||||
/>
|
||||
<Row>
|
||||
@@ -320,7 +320,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
"data": [
|
||||
{
|
||||
"id": "",
|
||||
"name": "数据集名称",
|
||||
"name": "知识库名称",
|
||||
"description": "描述信息",
|
||||
"permission": "only_me",
|
||||
"data_source_type": "upload_file",
|
||||
@@ -355,12 +355,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
此接口基于已存在数据集,在此数据集的基础上通过文本更新文档
|
||||
此接口基于已存在知识库,在此知识库的基础上通过文本更新文档
|
||||
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
文档 ID
|
||||
@@ -452,12 +452,12 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
此接口基于已存在数据集,在此数据集的基础上通过文件更新文档的操作。
|
||||
此接口基于已存在知识库,在此知识库的基础上通过文件更新文档的操作。
|
||||
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
文档 ID
|
||||
@@ -549,7 +549,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
<Property name='batch' type='string' key='batch'>
|
||||
上传文档的批次号
|
||||
@@ -604,7 +604,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
文档 ID
|
||||
@@ -638,7 +638,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Heading
|
||||
url='/datasets/{dataset_id}/documents'
|
||||
method='GET'
|
||||
title='数据集文档列表'
|
||||
title='知识库文档列表'
|
||||
name='#dataset_document_list'
|
||||
/>
|
||||
<Row>
|
||||
@@ -646,7 +646,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
@@ -721,7 +721,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
文档 ID
|
||||
@@ -732,7 +732,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Properties>
|
||||
<Property name='segments' type='object list' key='segments'>
|
||||
- <code>content</code> (text) 文本内容/问题内容,必填
|
||||
- <code>answer</code> (text) 答案内容,非必填,如果数据集的模式为qa模式则传值
|
||||
- <code>answer</code> (text) 答案内容,非必填,如果知识库的模式为qa模式则传值
|
||||
- <code>keywords</code> (list) 关键字,非必填
|
||||
</Property>
|
||||
</Properties>
|
||||
@@ -807,7 +807,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
<Property name='document_id' type='string' key='document_id'>
|
||||
文档 ID
|
||||
@@ -885,7 +885,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### Path
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
<Property name='segment_id' type='string' key='segment_id'>
|
||||
文档分段ID
|
||||
@@ -928,7 +928,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
### POST
|
||||
<Properties>
|
||||
<Property name='dataset_id' type='string' key='dataset_id'>
|
||||
数据集 ID
|
||||
知识库 ID
|
||||
</Property>
|
||||
<Property name='segment_id' type='string' key='segment_id'>
|
||||
文档分段ID
|
||||
@@ -939,7 +939,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<Properties>
|
||||
<Property name='segments' type='object list' key='segments'>
|
||||
- <code>content</code> (text) 文本内容/问题内容,必填
|
||||
- <code>answer</code> (text) 答案内容,非必填,如果数据集的模式为qa模式则传值
|
||||
- <code>answer</code> (text) 答案内容,非必填,如果知识库的模式为qa模式则传值
|
||||
- <code>keywords</code> (list) 关键字,非必填
|
||||
- <code>enabled</code> (bool) false/true,非必填
|
||||
</Property>
|
||||
@@ -1037,72 +1037,72 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
|
||||
<table className="max-w-auto border-collapse border border-slate-400" style={{ maxWidth: 'none', width: 'auto' }}>
|
||||
<thead style={{ background: '#f9fafc' }}>
|
||||
<tr>
|
||||
<th class="p-2 border border-slate-300">code</th>
|
||||
<th class="p-2 border border-slate-300">status</th>
|
||||
<th class="p-2 border border-slate-300">message</th>
|
||||
<th className="p-2 border border-slate-300">code</th>
|
||||
<th className="p-2 border border-slate-300">status</th>
|
||||
<th className="p-2 border border-slate-300">message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">no_file_uploaded</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">Please upload your file.</td>
|
||||
<td className="p-2 border border-slate-300">no_file_uploaded</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">Please upload your file.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">too_many_files</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">Only one file is allowed.</td>
|
||||
<td className="p-2 border border-slate-300">too_many_files</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">Only one file is allowed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">file_too_large</td>
|
||||
<td class="p-2 border border-slate-300">413</td>
|
||||
<td class="p-2 border border-slate-300">File size exceeded.</td>
|
||||
<td className="p-2 border border-slate-300">file_too_large</td>
|
||||
<td className="p-2 border border-slate-300">413</td>
|
||||
<td className="p-2 border border-slate-300">File size exceeded.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">unsupported_file_type</td>
|
||||
<td class="p-2 border border-slate-300">415</td>
|
||||
<td class="p-2 border border-slate-300">File type not allowed.</td>
|
||||
<td className="p-2 border border-slate-300">unsupported_file_type</td>
|
||||
<td className="p-2 border border-slate-300">415</td>
|
||||
<td className="p-2 border border-slate-300">File type not allowed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">high_quality_dataset_only</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">Current operation only supports 'high-quality' datasets.</td>
|
||||
<td className="p-2 border border-slate-300">high_quality_dataset_only</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">Current operation only supports 'high-quality' datasets.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">dataset_not_initialized</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">The dataset is still being initialized or indexing. Please wait a moment.</td>
|
||||
<td className="p-2 border border-slate-300">dataset_not_initialized</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">The dataset is still being initialized or indexing. Please wait a moment.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">archived_document_immutable</td>
|
||||
<td class="p-2 border border-slate-300">403</td>
|
||||
<td class="p-2 border border-slate-300">The archived document is not editable.</td>
|
||||
<td className="p-2 border border-slate-300">archived_document_immutable</td>
|
||||
<td className="p-2 border border-slate-300">403</td>
|
||||
<td className="p-2 border border-slate-300">The archived document is not editable.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">dataset_name_duplicate</td>
|
||||
<td class="p-2 border border-slate-300">409</td>
|
||||
<td class="p-2 border border-slate-300">The dataset name already exists. Please modify your dataset name.</td>
|
||||
<td className="p-2 border border-slate-300">dataset_name_duplicate</td>
|
||||
<td className="p-2 border border-slate-300">409</td>
|
||||
<td className="p-2 border border-slate-300">The dataset name already exists. Please modify your dataset name.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">invalid_action</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">Invalid action.</td>
|
||||
<td className="p-2 border border-slate-300">invalid_action</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">Invalid action.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">document_already_finished</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">The document has been processed. Please refresh the page or go to the document details.</td>
|
||||
<td className="p-2 border border-slate-300">document_already_finished</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">The document has been processed. Please refresh the page or go to the document details.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">document_indexing</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">The document is being processed and cannot be edited.</td>
|
||||
<td className="p-2 border border-slate-300">document_indexing</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">The document is being processed and cannot be edited.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="p-2 border border-slate-300">invalid_metadata</td>
|
||||
<td class="p-2 border border-slate-300">400</td>
|
||||
<td class="p-2 border border-slate-300">The metadata content is incorrect. Please check and verify.</td>
|
||||
<td className="p-2 border border-slate-300">invalid_metadata</td>
|
||||
<td className="p-2 border border-slate-300">400</td>
|
||||
<td className="p-2 border border-slate-300">The metadata content is incorrect. Please check and verify.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pb-4" />
|
||||
<div className="pb-4" />
|
||||
|
||||
@@ -6,11 +6,9 @@ const Layout: FC<{
|
||||
children: React.ReactNode
|
||||
}> = ({ children }) => {
|
||||
return (
|
||||
<div className=''>
|
||||
<div className="min-w-[300px]">
|
||||
<GA gaType={GaType.webapp} />
|
||||
{children}
|
||||
</div>
|
||||
<div className="min-w-[300px] h-full pb-[env(safe-area-inset-bottom)]">
|
||||
<GA gaType={GaType.webapp} />
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export type IAppBasicProps = {
|
||||
hoverTip?: string
|
||||
textStyle?: { main?: string; extra?: string }
|
||||
isExtraInLine?: boolean
|
||||
mode?: 'expand' | 'collapse'
|
||||
}
|
||||
|
||||
const ApiSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -31,18 +32,18 @@ const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" x
|
||||
</svg>
|
||||
|
||||
const WebappSvg = <svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
||||
const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6294_13848)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white"/>
|
||||
<path d="M8.43607 10.1842V10.0318C8.43607 9.64564 8.74535 9.32537 9.14397 9.29876L12.0475 9.10491L16.0628 15.0178V9.82823L15.0293 9.69046V9.6181C15.0293 9.22739 15.3456 8.90501 15.7493 8.88433L18.3912 8.74899V9.12918C18.3912 9.30765 18.2585 9.46031 18.0766 9.49108L17.4408 9.59861V18.0029L16.6429 18.2773C15.9764 18.5065 15.2343 18.2611 14.8527 17.6853L10.9545 11.803V17.4173L12.1544 17.647L12.1377 17.7583C12.0853 18.1069 11.7843 18.3705 11.4202 18.3867L8.43607 18.5195C8.39662 18.1447 8.67758 17.8093 9.06518 17.7686L9.45771 17.7273V10.2416L8.43607 10.1842Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5062 2.22521L3.5426 3.04201C2.82599 3.09094 2.27051 3.66706 2.27051 4.36136V15.9975C2.27051 16.6499 2.49507 17.2837 2.90883 17.7992L5.48835 21.0126C5.90541 21.5322 6.56174 21.8183 7.24076 21.7765L20.519 20.9591C21.1995 20.9172 21.7293 20.3716 21.7293 19.7125V6.48332C21.7293 6.07557 21.5234 5.69348 21.1777 5.45975L16.9743 2.61784C16.546 2.32822 16.0277 2.1896 15.5062 2.22521ZM4.13585 4.54287C3.96946 4.41968 4.04865 4.16303 4.25768 4.14804L15.5866 3.33545C15.9476 3.30956 16.3063 3.40896 16.5982 3.61578L18.8713 5.22622C18.9576 5.28736 18.9171 5.41935 18.8102 5.42516L6.8129 6.07764C6.44983 6.09739 6.09144 5.99073 5.80276 5.77699L4.13585 4.54287ZM6.25018 8.12315C6.25018 7.7334 6.56506 7.41145 6.9677 7.38952L19.6523 6.69871C20.0447 6.67734 20.375 6.97912 20.375 7.35898V18.8141C20.375 19.2031 20.0613 19.5247 19.6594 19.5476L7.05516 20.2648C6.61845 20.2896 6.25018 19.954 6.25018 19.5312V8.12315Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white" />
|
||||
<path d="M8.43607 10.1842V10.0318C8.43607 9.64564 8.74535 9.32537 9.14397 9.29876L12.0475 9.10491L16.0628 15.0178V9.82823L15.0293 9.69046V9.6181C15.0293 9.22739 15.3456 8.90501 15.7493 8.88433L18.3912 8.74899V9.12918C18.3912 9.30765 18.2585 9.46031 18.0766 9.49108L17.4408 9.59861V18.0029L16.6429 18.2773C15.9764 18.5065 15.2343 18.2611 14.8527 17.6853L10.9545 11.803V17.4173L12.1544 17.647L12.1377 17.7583C12.0853 18.1069 11.7843 18.3705 11.4202 18.3867L8.43607 18.5195C8.39662 18.1447 8.67758 17.8093 9.06518 17.7686L9.45771 17.7273V10.2416L8.43607 10.1842Z" fill="black" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5062 2.22521L3.5426 3.04201C2.82599 3.09094 2.27051 3.66706 2.27051 4.36136V15.9975C2.27051 16.6499 2.49507 17.2837 2.90883 17.7992L5.48835 21.0126C5.90541 21.5322 6.56174 21.8183 7.24076 21.7765L20.519 20.9591C21.1995 20.9172 21.7293 20.3716 21.7293 19.7125V6.48332C21.7293 6.07557 21.5234 5.69348 21.1777 5.45975L16.9743 2.61784C16.546 2.32822 16.0277 2.1896 15.5062 2.22521ZM4.13585 4.54287C3.96946 4.41968 4.04865 4.16303 4.25768 4.14804L15.5866 3.33545C15.9476 3.30956 16.3063 3.40896 16.5982 3.61578L18.8713 5.22622C18.9576 5.28736 18.9171 5.41935 18.8102 5.42516L6.8129 6.07764C6.44983 6.09739 6.09144 5.99073 5.80276 5.77699L4.13585 4.54287ZM6.25018 8.12315C6.25018 7.7334 6.56506 7.41145 6.9677 7.38952L19.6523 6.69871C20.0447 6.67734 20.375 6.97912 20.375 7.35898V18.8141C20.375 19.2031 20.0613 19.5247 19.6594 19.5476L7.05516 20.2648C6.61845 20.2896 6.25018 19.954 6.25018 19.5312V8.12315Z" fill="black" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6294_13848">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
@@ -55,7 +56,7 @@ const ICON_MAP = {
|
||||
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
|
||||
}
|
||||
|
||||
export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, iconType = 'app', isExtraInLine }: IAppBasicProps) {
|
||||
export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, mode = 'expand', iconType = 'app', isExtraInLine }: IAppBasicProps) {
|
||||
return (
|
||||
<div className="flex items-start">
|
||||
{icon && icon_background && iconType === 'app' && (
|
||||
@@ -69,7 +70,7 @@ export default function AppBasic({ icon, icon_background, name, type, hoverTip,
|
||||
</div>
|
||||
|
||||
}
|
||||
<div className="group">
|
||||
{mode === 'expand' && <div className="group">
|
||||
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 break-all ${textStyle?.main ?? ''}`}>
|
||||
{name}
|
||||
{hoverTip
|
||||
@@ -78,7 +79,7 @@ export default function AppBasic({ icon, icon_background, name, type, hoverTip,
|
||||
</Tooltip>}
|
||||
</div>
|
||||
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 break-all ${textStyle?.extra ?? ''}`}>{type}</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import NavLink from './navLink'
|
||||
import AppBasic from './basic'
|
||||
|
||||
import type { NavIcon } from './navLink'
|
||||
import AppBasic from './basic'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
export type IAppDetailNavProps = {
|
||||
iconType?: 'app' | 'dataset' | 'notion'
|
||||
@@ -20,15 +20,19 @@ export type IAppDetailNavProps = {
|
||||
}
|
||||
|
||||
const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInfo, iconType = 'app' }: IAppDetailNavProps) => {
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const mode = isMobile ? 'collapse' : 'expand'
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-56 overflow-y-auto bg-white border-r border-gray-200 shrink-0">
|
||||
<div className="flex flex-col sm:w-56 w-16 overflow-y-auto bg-white border-r border-gray-200 shrink-0 mobile:h-screen">
|
||||
<div className="flex flex-shrink-0 p-4">
|
||||
<AppBasic iconType={iconType} icon={icon} icon_background={icon_background} name={title} type={desc} />
|
||||
<AppBasic mode={mode} iconType={iconType} icon={icon} icon_background={icon_background} name={title} type={desc} />
|
||||
</div>
|
||||
<nav className="flex-1 p-4 space-y-1 bg-white">
|
||||
{navigation.map((item, index) => {
|
||||
return (
|
||||
<NavLink key={index} iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
|
||||
<NavLink key={index} mode={mode} iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
|
||||
)
|
||||
})}
|
||||
{extraInfo ?? null}
|
||||
|
||||
@@ -18,12 +18,14 @@ export type NavLinkProps = {
|
||||
selected: NavIcon
|
||||
normal: NavIcon
|
||||
}
|
||||
mode?: 'expand' | 'collapse'
|
||||
}
|
||||
|
||||
export default function NavLink({
|
||||
name,
|
||||
href,
|
||||
iconMap,
|
||||
mode = 'expand',
|
||||
}: NavLinkProps) {
|
||||
const segment = useSelectedLayoutSegment()
|
||||
const isActive = href.toLowerCase().split('/')?.pop() === segment?.toLowerCase()
|
||||
@@ -45,7 +47,7 @@ export default function NavLink({
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{name}
|
||||
{mode === 'expand' && name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ const Answer: FC<IAnswerProps> = ({
|
||||
{!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')}
|
||||
</div>
|
||||
</div>
|
||||
{more && <MoreInfo className='hidden group-hover:block' more={more} isQuestion={false} />}
|
||||
{more && <MoreInfo className='invisible group-hover:visible' more={more} isQuestion={false} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,3 +35,9 @@ export const TryToAskIcon = (
|
||||
<path d="M5.88889 0.683718C5.827 0.522805 5.67241 0.416626 5.5 0.416626C5.3276 0.416626 5.173 0.522805 5.11111 0.683718L4.27279 2.86334C4.14762 3.18877 4.10829 3.28255 4.05449 3.35821C4.00051 3.43413 3.93418 3.50047 3.85826 3.55445C3.78259 3.60825 3.68881 3.64758 3.36338 3.77275L1.18376 4.61106C1.02285 4.67295 0.916668 4.82755 0.916668 4.99996C0.916668 5.17236 1.02285 5.32696 1.18376 5.38885L3.36338 6.22717C3.68881 6.35234 3.78259 6.39167 3.85826 6.44547C3.93418 6.49945 4.00051 6.56578 4.05449 6.6417C4.10829 6.71737 4.14762 6.81115 4.27279 7.13658L5.11111 9.3162C5.173 9.47711 5.3276 9.58329 5.5 9.58329C5.67241 9.58329 5.82701 9.47711 5.8889 9.3162L6.72721 7.13658C6.85238 6.81115 6.89171 6.71737 6.94551 6.6417C6.99949 6.56578 7.06583 6.49945 7.14175 6.44547C7.21741 6.39167 7.31119 6.35234 7.63662 6.22717L9.81624 5.38885C9.97715 5.32696 10.0833 5.17236 10.0833 4.99996C10.0833 4.82755 9.97715 4.67295 9.81624 4.61106L7.63662 3.77275C7.31119 3.64758 7.21741 3.60825 7.14175 3.55445C7.06583 3.50047 6.99949 3.43413 6.94551 3.35821C6.89171 3.28255 6.85238 3.18877 6.72721 2.86334L5.88889 0.683718Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ReplayIcon = ({ className }: SVGProps<SVGElement>) => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.33301 6.66667C1.33301 6.66667 2.66966 4.84548 3.75556 3.75883C4.84147 2.67218 6.34207 2 7.99967 2C11.3134 2 13.9997 4.68629 13.9997 8C13.9997 11.3137 11.3134 14 7.99967 14C5.26428 14 2.95642 12.1695 2.23419 9.66667M1.33301 6.66667V2.66667M1.33301 6.66667H5.33301" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { DataSet } from '@/models/datasets'
|
||||
import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader'
|
||||
import ImageList from '@/app/components/base/image-uploader/image-list'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
import { useImageFiles } from '@/app/components/base/image-uploader/hooks'
|
||||
import { useClipboardUploader, useDraggableUploader, useImageFiles } from '@/app/components/base/image-uploader/hooks'
|
||||
|
||||
export type IChatProps = {
|
||||
configElem?: React.ReactNode
|
||||
@@ -101,6 +101,8 @@ const Chat: FC<IChatProps> = ({
|
||||
onImageLinkLoadSuccess,
|
||||
onClear,
|
||||
} = useImageFiles()
|
||||
const { onPaste } = useClipboardUploader({ onUpload, visionConfig, files })
|
||||
const { onDragEnter, onDragLeave, onDragOver, onDrop, isDragActive } = useDraggableUploader<HTMLTextAreaElement>({ onUpload, files, visionConfig })
|
||||
const isUseInputMethod = useRef(false)
|
||||
|
||||
const [query, setQuery] = React.useState('')
|
||||
@@ -272,7 +274,7 @@ const Chat: FC<IChatProps> = ({
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
<div className='p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto'>
|
||||
<div className={cn('p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto', isDragActive && 'border-primary-600')}>
|
||||
{
|
||||
visionConfig?.enabled && (
|
||||
<>
|
||||
@@ -305,6 +307,11 @@ const Chat: FC<IChatProps> = ({
|
||||
onChange={handleContentChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={onPaste}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
autoSize
|
||||
/>
|
||||
<div className="absolute bottom-2 right-2 flex items-center h-8">
|
||||
|
||||
@@ -27,6 +27,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-page/model-selector'
|
||||
import { ModelType, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import type { ModelModeType } from '@/types/app'
|
||||
export type IConfigModelProps = {
|
||||
isAdvancedMode: boolean
|
||||
@@ -54,6 +55,10 @@ const ConfigModel: FC<IConfigModelProps> = ({
|
||||
const [maxTokenSettingTipVisible, setMaxTokenSettingTipVisible] = useState(false)
|
||||
const configContentRef = React.useRef(null)
|
||||
const currModel = textGenerationModelList.find(item => item.model_name === modelId)
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
// Cache loaded model param
|
||||
const [allParams, setAllParams, getAllParams] = useGetState<Record<string, Record<string, any>>>({})
|
||||
const currParams = allParams[provider]?.[modelId]
|
||||
@@ -288,7 +293,7 @@ const ConfigModel: FC<IConfigModelProps> = ({
|
||||
</div>
|
||||
{isShowConfig && (
|
||||
<Panel
|
||||
className='absolute z-20 top-8 right-0 !w-[496px] bg-white !overflow-visible shadow-md'
|
||||
className='absolute z-20 top-8 left-0 sm:left-[unset] sm:right-0 !w-fit sm:!w-[496px] bg-white !overflow-visible shadow-md'
|
||||
keepUnFold
|
||||
headerIcon={
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -340,7 +345,7 @@ const ConfigModel: FC<IConfigModelProps> = ({
|
||||
<div className='grow flex items-center' key={tone.id}>
|
||||
<Radio
|
||||
value={tone.id}
|
||||
className={cn(tone.id === toneId && 'rounded-md border border-gray-200 shadow-md', '!mr-0 grow !px-2 !justify-center text-[13px] font-medium')}
|
||||
className={cn(tone.id === toneId && 'rounded-md border border-gray-200 shadow-md', '!mr-0 grow !px-1 sm:!px-2 !justify-center text-[13px] font-medium')}
|
||||
labelClassName={cn(tone.id === toneId
|
||||
? ({
|
||||
1: 'text-[#6938EF]',
|
||||
@@ -351,7 +356,7 @@ const ConfigModel: FC<IConfigModelProps> = ({
|
||||
>
|
||||
<>
|
||||
{getToneIcon(tone.id)}
|
||||
<div>{t(`common.model.tone.${tone.name}`) as string}</div>
|
||||
{!isMobile && <div>{t(`common.model.tone.${tone.name}`) as string}</div>}
|
||||
<div className=""></div>
|
||||
</>
|
||||
</Radio>
|
||||
@@ -361,12 +366,12 @@ const ConfigModel: FC<IConfigModelProps> = ({
|
||||
</>
|
||||
<Radio
|
||||
value={TONE_LIST[3].id}
|
||||
className={cn(toneId === 4 && 'rounded-md border border-gray-200 shadow-md', '!mr-0 grow !px-2 !justify-center text-[13px] font-medium')}
|
||||
className={cn(toneId === 4 && 'rounded-md border border-gray-200 shadow-md', '!mr-0 grow !px-1 sm:!px-2 !justify-center text-[13px] font-medium')}
|
||||
labelClassName={cn('flex items-center space-x-2 ', toneId === 4 ? 'text-[#155EEF]' : 'text-[#667085]')}
|
||||
>
|
||||
<>
|
||||
{getToneIcon(TONE_LIST[3].id)}
|
||||
<div>{t(`common.model.tone.${TONE_LIST[3].name}`) as string}</div>
|
||||
{!isMobile && <div>{t(`common.model.tone.${TONE_LIST[3].name}`) as string}</div>}
|
||||
</>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
|
||||
@@ -20,7 +20,7 @@ const ModelModeTypeLabel: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(className, isHighlight ? 'border-indigo-300 text-indigo-600' : 'border-gray-300 text-gray-500', 'flex items-center h-4 px-1 border rounded text-xs font-semibold uppercase')}
|
||||
className={cn(className, isHighlight ? 'border-indigo-300 text-indigo-600' : 'border-gray-300 text-gray-500', 'flex items-center h-4 px-1 border rounded text-xs font-semibold uppercase text-ellipsis overflow-hidden whitespace-nowrap')}
|
||||
>
|
||||
{t(`appDebug.modelConfig.modeType.${type}`)}
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ const ModelName: FC<IModelNameProps> = ({
|
||||
modelDisplayName,
|
||||
}) => {
|
||||
return (
|
||||
<span title={modelDisplayName}>
|
||||
<span className='text-ellipsis overflow-hidden whitespace-nowrap' title={modelDisplayName}>
|
||||
{modelDisplayName}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -49,7 +49,7 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max
|
||||
onChange(id, getFitPrecisionValue(value, precision))
|
||||
}, [value, precision])
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between flex-wrap gap-y-2">
|
||||
<div className="flex flex-col flex-shrink-0">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-[6px] text-gray-500 text-[13px] font-medium">{name}</span>
|
||||
|
||||
@@ -185,8 +185,8 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.notSetVar')}</div>
|
||||
)}
|
||||
{hasVar && (
|
||||
<div className='rounded-lg border border-gray-200 bg-white'>
|
||||
<table className={`${s.table} w-full border-collapse border-0 rounded-lg text-sm`}>
|
||||
<div className='rounded-lg border border-gray-200 bg-white overflow-x-auto'>
|
||||
<table className={`${s.table} min-w-[440px] w-full max-w-full border-collapse border-0 rounded-lg text-sm`}>
|
||||
<thead className="border-b border-gray-200 text-gray-500 text-xs font-medium">
|
||||
<tr className='uppercase'>
|
||||
<td>{t('appDebug.variableTable.key')}</td>
|
||||
|
||||
@@ -31,7 +31,7 @@ const ParamsConfig: FC = () => {
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 50 }}>
|
||||
<div className='w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
|
||||
<div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
|
||||
<ParamConfigContent />
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
||||
@@ -16,6 +16,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
// type
|
||||
import type { AutomaticRes } from '@/service/debug'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
const noDataIcon = (
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -47,6 +48,9 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const [audiences, setAudiences] = React.useState<string>('')
|
||||
const [hopingToSolve, setHopingToSolve] = React.useState<string>('')
|
||||
const isValid = () => {
|
||||
@@ -103,15 +107,36 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
|
||||
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
|
||||
|
||||
const isShowAutoPromptInput = () => {
|
||||
if (isMobile) {
|
||||
// hide prompt panel on mobile if it is loading or has had result
|
||||
if (isLoading || res)
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
// alway display prompt panel on desktop mode
|
||||
return true
|
||||
}
|
||||
|
||||
const isShowAutoPromptResPlaceholder = () => {
|
||||
if (isMobile) {
|
||||
// hide placeholder panel on mobile
|
||||
return false
|
||||
}
|
||||
|
||||
return !isLoading && !res
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className='min-w-[1120px] !p-0'
|
||||
className='!p-0 sm:min-w-[768px] xl:min-w-[1120px]'
|
||||
closable
|
||||
>
|
||||
<div className='flex h-[680px]'>
|
||||
<div className='w-[480px] shrink-0 px-8 py-6 h-full overflow-y-auto border-r border-gray-100'>
|
||||
<div className='flex h-[680px] flex-wrap gap-y-4 overflow-y-auto'>
|
||||
{isShowAutoPromptInput() && <div className='w-full sm:w-[360px] xl:w-[480px] shrink-0 px-8 py-6 h-full overflow-y-auto border-r border-gray-100'>
|
||||
<div>
|
||||
<div className='mb-1 text-xl font-semibold text-primary-600'>{t('appDebug.automatic.title')}</div>
|
||||
<div className='text-[13px] font-normal text-gray-500'>{t('appDebug.automatic.description')}</div>
|
||||
@@ -139,7 +164,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{(!isLoading && res) && (
|
||||
<div className='grow px-8 pt-6 h-full overflow-y-auto'>
|
||||
@@ -180,7 +205,7 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{isLoading && renderLoading}
|
||||
{(!isLoading && !res) && renderNoData}
|
||||
{isShowAutoPromptResPlaceholder() && renderNoData}
|
||||
{showConfirmOverwrite && (
|
||||
<Confirm
|
||||
title={t('appDebug.automatic.overwriteTitle')}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { formatNumber } from '@/utils/format'
|
||||
import FileIcon from '@/app/components/base/file-icon'
|
||||
import { Settings01, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
type ItemProps = {
|
||||
className?: string
|
||||
@@ -24,6 +26,10 @@ const Item: FC<ItemProps> = ({
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
|
||||
const handleSave = (newDataset: DataSet) => {
|
||||
@@ -74,15 +80,13 @@ const Item: FC<ItemProps> = ({
|
||||
<Trash03 className='w-4 h-4 text-gray-500 group-hover/action:text-[#D92D20]' />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showSettingsModal && (
|
||||
<SettingsModal
|
||||
currentDataset={config}
|
||||
onCancel={() => setShowSettingsModal(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Drawer isOpen={showSettingsModal} onClose={() => setShowSettingsModal(false)} footer={null} mask={isMobile} panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'>
|
||||
<SettingsModal
|
||||
currentDataset={config}
|
||||
onCancel={() => setShowSettingsModal(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ const ParamsConfig: FC = () => {
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
className='min-w-[528px]'
|
||||
className='sm:min-w-[528px]'
|
||||
wrapperClassName='z-50'
|
||||
title={t('appDebug.datasetConfig.settingTitle')}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { FC } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import cn from 'classnames'
|
||||
@@ -30,7 +29,7 @@ type SettingsModalProps = {
|
||||
}
|
||||
|
||||
const rowClass = `
|
||||
flex justify-between py-4
|
||||
flex justify-between py-4 flex-wrap gap-y-2
|
||||
`
|
||||
|
||||
const labelClass = `
|
||||
@@ -45,10 +44,6 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const ref = useRef(null)
|
||||
useClickAway(() => {
|
||||
if (ref)
|
||||
onCancel()
|
||||
}, ref)
|
||||
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -122,10 +117,8 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='fixed top-16 right-2 flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl z-10'
|
||||
className='overflow-hidden w-full flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl'
|
||||
style={{
|
||||
zIndex: 11,
|
||||
width: 700,
|
||||
height: 'calc(100vh - 72px)',
|
||||
}}
|
||||
ref={ref}
|
||||
@@ -179,12 +172,12 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
<div className={labelClass}>
|
||||
<div>{t('datasetSettings.form.permissions')}</div>
|
||||
</div>
|
||||
<div className='w-[480px]'>
|
||||
<div className='w-full sm:w-[480px]'>
|
||||
<PermissionsRadio
|
||||
disable={!localeCurrentDataset?.embedding_available}
|
||||
value={localeCurrentDataset.permission}
|
||||
onChange={v => handleValueChange('permission', v!)}
|
||||
itemClassName='!w-[227px]'
|
||||
itemClassName='sm:!w-[227px]'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,7 +191,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
disable={!localeCurrentDataset?.embedding_available}
|
||||
value={indexMethod}
|
||||
onChange={v => setIndexMethod(v!)}
|
||||
itemClassName='!w-[227px]'
|
||||
itemClassName='sm:!w-[227px]'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,7 +265,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
)}
|
||||
|
||||
<div
|
||||
className='absolute z-[5] bottom-0 w-full flex justify-end py-4 px-6 border-t bg-white '
|
||||
className='sticky z-[5] bottom-0 w-full flex justify-end py-4 px-6 border-t bg-white '
|
||||
style={{
|
||||
borderColor: 'rgba(0, 0, 0, 0.05)',
|
||||
}}
|
||||
|
||||
@@ -8,6 +8,7 @@ import produce from 'immer'
|
||||
import { useBoolean, useGetState } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { clone, isEqual } from 'lodash-es'
|
||||
import { CodeBracketIcon } from '@heroicons/react/20/solid'
|
||||
import Button from '../../base/button'
|
||||
import Loading from '../../base/loading'
|
||||
import s from './style.module.css'
|
||||
@@ -44,6 +45,8 @@ import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/
|
||||
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
|
||||
type PublichConfig = {
|
||||
modelConfig: ModelConfig
|
||||
@@ -64,6 +67,10 @@ const Configuration: FC = () => {
|
||||
|
||||
const [conversationId, setConversationId] = useState<string | null>('')
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const [isShowDebugPanel, { setTrue: showDebugPanel, setFalse: hideDebugPanel }] = useBoolean(false)
|
||||
|
||||
const [introduction, setIntroduction] = useState<string>('')
|
||||
const [controlClearChatMessage, setControlClearChatMessage] = useState(0)
|
||||
const [prevPromptConfig, setPrevPromptConfig] = useState<PromptConfig>({
|
||||
@@ -600,7 +607,7 @@ const Configuration: FC = () => {
|
||||
>
|
||||
<>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className='flex items-center justify-between px-6 shrink-0 h-14'>
|
||||
<div className='flex items-center justify-between px-6 shrink-0 py-3 flex-wrap gap-y-2'>
|
||||
<div className='flex items-end'>
|
||||
<div className={s.promptTitle}></div>
|
||||
<div className='flex items-center h-[14px] space-x-1 text-xs'>
|
||||
@@ -630,7 +637,7 @@ const Configuration: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-center flex-wrap gap-y-2 gap-x-2'>
|
||||
{/* Model and Parameters */}
|
||||
<ConfigModel
|
||||
isAdvancedMode={isAdvancedMode}
|
||||
@@ -644,22 +651,28 @@ const Configuration: FC = () => {
|
||||
}}
|
||||
disabled={!hasSetAPIKEY}
|
||||
/>
|
||||
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
|
||||
<div className='w-[1px] h-[14px] bg-gray-200'></div>
|
||||
<Button onClick={() => setShowConfirm(true)} className='shrink-0 mr-2 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.resetConfig')}</Button>
|
||||
{isMobile && (
|
||||
<Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
|
||||
<span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
|
||||
<CodeBracketIcon className="h-4 w-4 text-gray-500" />
|
||||
</Button>
|
||||
)}
|
||||
<Button type='primary' onClick={() => handlePublish(false)} className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}>{t('appDebug.operation.applyConfig')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex grow h-[200px]'>
|
||||
<div className="w-1/2 min-w-[560px] shrink-0">
|
||||
<div className="w-full sm:w-1/2 shrink-0">
|
||||
<Config />
|
||||
</div>
|
||||
<div className="relative w-1/2 grow h-full overflow-y-auto py-4 px-6 bg-gray-50 flex flex-col rounded-tl-2xl border-t border-l" style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||
{!isMobile && <div className="relative w-1/2 grow h-full overflow-y-auto py-4 px-6 bg-gray-50 flex flex-col rounded-tl-2xl border-t border-l" style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||
<Debug
|
||||
hasSetAPIKEY={hasSetAPIKEY}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
inputs={inputs}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
{showConfirm && (
|
||||
@@ -707,6 +720,15 @@ const Configuration: FC = () => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isMobile && (
|
||||
<Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
|
||||
<Debug
|
||||
hasSetAPIKEY={hasSetAPIKEY}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
inputs={inputs}
|
||||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
</>
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ const Filter: FC<IFilterProps> = ({ appId, queryParams, setQueryParams }: IFilte
|
||||
if (!data)
|
||||
return null
|
||||
return (
|
||||
<div className='flex flex-row items-center mb-4 text-gray-900 text-base'>
|
||||
<div className='flex flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
|
||||
<SimpleSelect
|
||||
items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
|
||||
className='mt-0 !w-40'
|
||||
@@ -47,7 +47,7 @@ const Filter: FC<IFilterProps> = ({ appId, queryParams, setQueryParams }: IFilte
|
||||
setQueryParams({ ...queryParams, period: item.value })
|
||||
}}
|
||||
defaultValue={queryParams.period} />
|
||||
<div className="relative ml-4 rounded-md mr-4">
|
||||
<div className="relative rounded-md">
|
||||
<SimpleSelect
|
||||
defaultValue={'all'}
|
||||
className='!w-[300px]'
|
||||
|
||||
@@ -33,6 +33,7 @@ import { TONE_LIST } from '@/config'
|
||||
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
|
||||
import ModelName from '@/app/components/app/configuration/config-model/model-name'
|
||||
import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
type IConversationList = {
|
||||
logs?: ChatConversationsResponse | CompletionConversationsResponse
|
||||
@@ -200,7 +201,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
|
||||
<div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-center flex-wrap gap-y-1 justify-end'>
|
||||
<div
|
||||
className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
|
||||
>
|
||||
@@ -412,6 +413,10 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
|
||||
*/
|
||||
const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
|
||||
const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
|
||||
const isChatMode = appDetail?.mode === 'chat' // Whether the app is a chat app
|
||||
@@ -445,17 +450,17 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className={`w-full border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
|
||||
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
|
||||
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
|
||||
<tr>
|
||||
<td className='w-[1.375rem]'></td>
|
||||
<td>{t('appLog.table.header.time')}</td>
|
||||
<td>{t('appLog.table.header.endUser')}</td>
|
||||
<td>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
|
||||
<td>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
|
||||
<td>{t('appLog.table.header.userRate')}</td>
|
||||
<td>{t('appLog.table.header.adminRate')}</td>
|
||||
<td className='w-[1.375rem] whitespace-nowrap'></td>
|
||||
<td className='whitespace-nowrap'>{t('appLog.table.header.time')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
|
||||
<td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
|
||||
<td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-500">
|
||||
@@ -504,9 +509,9 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
<Drawer
|
||||
isOpen={showDrawer}
|
||||
onClose={onCloseDrawer}
|
||||
mask={false}
|
||||
mask={isMobile}
|
||||
footer={null}
|
||||
panelClassname='mt-16 mr-2 mb-3 !p-0 !max-w-[640px] rounded-b-xl'
|
||||
panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'
|
||||
>
|
||||
<DrawerContext.Provider value={{
|
||||
onClose: onCloseDrawer,
|
||||
@@ -518,7 +523,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
}
|
||||
</DrawerContext.Provider>
|
||||
</Drawer>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ function AppCard({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`min-w-max shadow-xs border-[0.5px] rounded-lg border-gray-200 ${
|
||||
className={`shadow-xs border-[0.5px] rounded-lg border-gray-200 ${
|
||||
className ?? ''
|
||||
}`}
|
||||
>
|
||||
@@ -163,8 +163,8 @@ function AppCard({
|
||||
: t('appOverview.overview.apiInfo.accessibleAddress')}
|
||||
</div>
|
||||
<div className="w-full h-9 pl-2 pr-0.5 py-0.5 bg-black bg-opacity-[0.02] rounded-lg border border-black border-opacity-5 justify-start items-center inline-flex">
|
||||
<div className="h-4 px-2 justify-start items-start gap-2 flex flex-1">
|
||||
<div className="text-gray-700 text-xs font-medium">
|
||||
<div className="h-4 px-2 justify-start items-start gap-2 flex flex-1 min-w-0">
|
||||
<div className="text-gray-700 text-xs font-medium text-ellipsis overflow-hidden whitespace-nowrap">
|
||||
{isApp ? appUrl : apiUrl}
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,7 +196,7 @@ function AppCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'pt-2 flex flex-row items-center'}>
|
||||
<div className={'pt-2 flex flex-row items-center flex-wrap gap-y-2'}>
|
||||
{!isApp && <SecretKeyButton className='flex-shrink-0 !h-8 bg-white mr-2' textCls='!text-gray-700 font-medium' iconCls='stroke-[1.2px]' appId={appInfo.id} />}
|
||||
{OPERATIONS_MAP[cardType].map((op) => {
|
||||
const disabled
|
||||
|
||||
@@ -81,10 +81,10 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
</div>
|
||||
<div className='flex py-4'>
|
||||
<StepNum>3</StepNum>
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className='flex flex-col w-full overflow-hidden'>
|
||||
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step3`)}</div>
|
||||
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step3Tip`)}</div>
|
||||
<pre className='box-border py-3 px-4 bg-gray-100 text-xs font-medium rounded-lg select-text'>
|
||||
<pre className='overflow-x-scroll box-border py-3 px-4 bg-gray-100 text-xs font-medium rounded-lg select-text'>
|
||||
NEXT_PUBLIC_APP_ID={`'${appId}'`} <br />
|
||||
NEXT_PUBLIC_APP_KEY={'\'<Web API Key From Dify>\''} <br />
|
||||
NEXT_PUBLIC_API_URL={`'${api_base_url}'`}
|
||||
|
||||
@@ -106,7 +106,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
|
||||
<div className="mb-4 mt-8 text-gray-900 text-[14px] font-medium leading-tight">
|
||||
{t(`${prefixEmbedded}.explanation`)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between flex-wrap gap-y-2">
|
||||
{Object.keys(OPTION_MAP).map((v, index) => {
|
||||
return (
|
||||
<div
|
||||
@@ -150,7 +150,7 @@ const Embedded = ({ isShow, onClose, appBaseUrl, accessToken }: Props) => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="self-stretch p-3 justify-start items-start gap-2 inline-flex">
|
||||
<div className="p-3 justify-start items-start gap-2 flex overflow-x-auto w-full">
|
||||
<div className="grow shrink basis-0 text-slate-700 text-[13px] leading-tight font-mono">
|
||||
<pre className='select-text'>{OPTION_MAP[option].getContent(appBaseUrl, accessToken, isTestEnv)}</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import Button from '../button'
|
||||
|
||||
type DrawerProps = {
|
||||
export type IDrawerProps = {
|
||||
title?: string
|
||||
description?: string
|
||||
panelClassname?: string
|
||||
@@ -12,6 +13,7 @@ type DrawerProps = {
|
||||
mask?: boolean
|
||||
isOpen: boolean
|
||||
// closable: boolean
|
||||
showClose?: boolean
|
||||
onClose: () => void
|
||||
onCancel?: () => void
|
||||
onOk?: () => void
|
||||
@@ -24,11 +26,12 @@ export default function Drawer({
|
||||
children,
|
||||
footer,
|
||||
mask = true,
|
||||
showClose = false,
|
||||
isOpen,
|
||||
onClose,
|
||||
onCancel,
|
||||
onOk,
|
||||
}: DrawerProps) {
|
||||
}: IDrawerProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Dialog
|
||||
@@ -52,6 +55,9 @@ export default function Drawer({
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>}
|
||||
{showClose && <Dialog.Title className="flex items-center mb-4" as="div">
|
||||
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
|
||||
</Dialog.Title>}
|
||||
{description && <Dialog.Description className='text-gray-500 text-xs font-normal mt-2'>{description}</Dialog.Description>}
|
||||
{children}
|
||||
</>
|
||||
|
||||
37
web/app/components/base/float-popover-container/index.tsx
Normal file
37
web/app/components/base/float-popover-container/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { PortalToFollowElemOptions } from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
type IFloatRightContainerProps = {
|
||||
isMobile: boolean
|
||||
open: boolean
|
||||
toggle: () => void
|
||||
triggerElement?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
} & PortalToFollowElemOptions
|
||||
|
||||
const FloatRightContainer = ({ open, toggle, triggerElement, isMobile, children, ...portalProps }: IFloatRightContainerProps) => {
|
||||
return (
|
||||
<>
|
||||
{isMobile && (
|
||||
<PortalToFollowElem open={open} {...portalProps}>
|
||||
<PortalToFollowElemTrigger onClick={toggle}>
|
||||
{triggerElement}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent>
|
||||
{children}
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)}
|
||||
{!isMobile && open && (
|
||||
<>{children}</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FloatRightContainer
|
||||
23
web/app/components/base/float-right-container/index.tsx
Normal file
23
web/app/components/base/float-right-container/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import type { IDrawerProps } from '@/app/components/base/drawer'
|
||||
|
||||
type IFloatRightContainerProps = {
|
||||
isMobile: boolean
|
||||
children?: React.ReactNode
|
||||
} & IDrawerProps
|
||||
|
||||
const FloatRightContainer = ({ isMobile, children, isOpen, ...drawerProps }: IFloatRightContainerProps) => {
|
||||
return (
|
||||
<>
|
||||
{isMobile && (
|
||||
<Drawer isOpen={isOpen} {...drawerProps}>{children}</Drawer>
|
||||
)}
|
||||
{(!isMobile && isOpen) && (
|
||||
<>{children}</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FloatRightContainer
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { FC } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Script from 'next/script'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
export enum GaType {
|
||||
admin = 'admin',
|
||||
@@ -11,27 +13,34 @@ const gaIdMaps = {
|
||||
[GaType.webapp]: 'G-2MFWXK7WYT',
|
||||
}
|
||||
|
||||
export interface IGAProps {
|
||||
export type IGAProps = {
|
||||
gaType: GaType
|
||||
}
|
||||
|
||||
|
||||
const GA: FC<IGAProps> = ({
|
||||
gaType
|
||||
gaType,
|
||||
}) => {
|
||||
if (IS_CE_EDITION)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Script
|
||||
id="gtag-base"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer', '${gaIdMaps[gaType]}');
|
||||
<>
|
||||
<Script strategy="beforeInteractive" async src={`https://www.googletagmanager.com/gtag/js?id=${gaIdMaps[gaType]}`}></Script>
|
||||
<Script
|
||||
id="ga-init"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gaIdMaps[gaType]}');
|
||||
`,
|
||||
}} />
|
||||
}}
|
||||
>
|
||||
</Script>
|
||||
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(GA)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user