Compare commits

..

42 Commits

Author SHA1 Message Date
takatost
bee0d12455 fix: remove postgresql default charset config (#1720) 2023-12-08 13:22:04 +08:00
takatost
13f2c90a7b bump version to 0.3.33 (#1719) 2023-12-08 13:13:21 +08:00
crazywoola
a3dca3dabc fix: auto generate type error in controllers/web/conversation.py (#1718) 2023-12-08 11:15:07 +08:00
SM-Tech
e5c7a81ce3 fix ascii codec error, by using utf8 (#1608)
Co-authored-by: crazywoola <427733928@qq.com>
2023-12-08 09:05:58 +08:00
crazywoola
8b0100523b Feat/regenrate conversation in embeded window (#1708) 2023-12-07 13:17:07 +08:00
zxhlyh
1350599c0b fix: process document priority tip (#1712) 2023-12-07 10:38:33 +08:00
Pascal M
bc54cdc537 refactor: typo in dataset docstore (#1711) 2023-12-07 09:24:52 +08:00
Pascal M
5d10cf0fe6 fix: error Class 'builtins.list' is not mapped (#1710) 2023-12-07 09:24:39 +08:00
Garfield Dai
7b8a10f3ea feat: billing enhancement 20231204 (#1691)
Co-authored-by: jyong <jyong@dify.ai>
2023-12-05 16:53:55 +08:00
zxhlyh
cb3a55dae6 feat: remove documents limit (#1697) 2023-12-05 16:53:40 +08:00
Rhon Joe
5789d76582 fix(web): reserve default copy behavior (#1693) 2023-12-05 16:34:12 +08:00
Joel
2e588ae221 feat: use gtag instead gtm (#1695) 2023-12-05 15:05:05 +08:00
Joel
b5dd948e56 feat: add upgrade ga test (#1690) 2023-12-04 18:16:59 +08:00
Garfield Dai
1263b7de75 fix: vector_size convert bytes to MB. (#1684) 2023-12-04 10:51:40 +08:00
Joel
75a6122173 feat: SaaS price plan frontend (#1683)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
2023-12-03 22:10:16 +08:00
Garfield Dai
053102f433 Feat/dify billing (#1679)
Co-authored-by: jyong <jyong@dify.ai>
Co-authored-by: takatost <takatost@users.noreply.github.com>
2023-12-03 20:59:29 +08:00
Yeuoly
d3a2c0ed34 fix wrong syntax of type definitions (#1678) 2023-12-03 20:59:13 +08:00
dependabot[bot]
8fbc374f31 chore(deps): bump word-wrap from 1.2.3 to 1.2.5 in /web (#1681)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-03 20:50:01 +08:00
dependabot[bot]
08b7ebba91 chore(deps): bump semver from 5.7.1 to 5.7.2 in /web (#1680)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-03 20:49:52 +08:00
Wen O.Y
a1cd043fdc fix: Incorrect order of embedded documents in CacheEmbedding (#1671) 2023-12-03 19:07:00 +08:00
crazywoola
671a8e7972 Doc/use proper links (#1673) 2023-12-02 14:08:10 +08:00
Yuhao
efa16dbb44 feat: drag to upload image (#1666) 2023-12-01 16:50:22 +08:00
simpx
a6241be42a fix: Fix typo in documentation: change 'converation' to 'conversation' (#1665) 2023-12-01 13:12:29 +08:00
Yuhao
faa88aafe8 feat: clipboard paste (#1663) 2023-12-01 10:04:14 +08:00
kimjion
1b3a98425f fix: app setting click pop (#1660) 2023-12-01 09:44:32 +08:00
WangBooth
22bc9ddc73 Hotfix/fix documents index mismatch error in rerank (#1662)
Co-authored-by: baomi.wbm <baomi.wbm@dtwave-inc.com>
2023-11-30 22:03:20 +08:00
Joel
0423775687 fix: explore page header menu hide in safari (#1658) 2023-11-30 16:13:42 +08:00
zxhlyh
307c170fb6 fix: Jina AI logo (#1656) 2023-11-30 15:41:59 +08:00
Neko Ayaka
0e04fcc071 chore(docker-compose): use proper comment indentation for users to easily toggle on and off features (#1648)
Signed-off-by: Neko Ayaka <neko@ayaka.moe>
2023-11-30 09:46:36 +08:00
Yuhao
4322b17a81 fix: app card hover selector (#1646) 2023-11-29 14:58:27 +08:00
zxhlyh
451af66be0 feat: add jina embedding (#1647)
Co-authored-by: takatost <takatost@gmail.com>
2023-11-29 14:58:11 +08:00
crazywoola
454577c6b1 Remove legacy docker startup docs in frontend (#1645) 2023-11-29 13:26:35 +08:00
WangBooth
53be4d2712 fix #1637 (#1638)
Co-authored-by: baomi.wbm <baomi.wbm@dtwave-inc.com>
2023-11-28 20:05:50 +08:00
Yuhao
3c37fd37fa fix: batch mobile layout fixes (#1641) 2023-11-28 20:05:19 +08:00
crazywoola
cf0ba794d7 fix: old webapp url still valid (#1643) 2023-11-28 20:04:46 +08:00
Panmuse
c21e2063fe Update README.md (#1636) 2023-11-28 15:23:05 +08:00
crazywoola
ad037c6615 feat: add items (#1633) 2023-11-27 19:38:00 +08:00
Joel
7bbfac5dba Chore: change dataset's i18n to knowledge (#1629) 2023-11-27 17:22:16 +08:00
zxhlyh
80ddb00f10 fix: score_threshold_enabled variable (#1627) 2023-11-27 15:38:05 +08:00
Jyong
74b2260ba6 fix score_threshold_enabled name (#1626)
Co-authored-by: jyong <jyong@dify.ai>
2023-11-27 15:34:45 +08:00
Panmuse
603e55f252 Update README.md (#1623) 2023-11-27 14:20:25 +08:00
Yuhao
a9c1c7d239 feat: fe mobile responsive next (#1609) 2023-11-27 11:47:48 +08:00
263 changed files with 4667 additions and 1193 deletions

View File

@@ -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

View File

@@ -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.**
![](./images/demo.png)

View File

@@ -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=

View File

@@ -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!

View File

@@ -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.

View File

@@ -28,3 +28,5 @@ from .universal_chat import chat, conversation, message, parameter, audio
# Import webhook controllers
from .webhook import stripe
from .billing import billing

View File

@@ -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()

View File

@@ -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:

View File

@@ -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:

View 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')

View File

@@ -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>')

View File

@@ -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')

View File

@@ -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)

View File

@@ -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:

View File

@@ -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')

View File

@@ -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:

View File

@@ -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:

View File

@@ -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')

View File

@@ -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

View File

@@ -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:

View File

@@ -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 = {}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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:

View File

@@ -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.')

View File

@@ -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,

View File

@@ -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)

View File

@@ -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]:

View File

@@ -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]:

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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 = []

View File

@@ -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)

View 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

View File

@@ -14,5 +14,6 @@
"xinference",
"openllm",
"localai",
"cohere"
"cohere",
"jina"
]

View File

@@ -0,0 +1,10 @@
{
"support_provider_types": [
"custom"
],
"system_config": null,
"model_flexibility": "fixed",
"supported_model_types": [
"embeddings"
]
}

View File

@@ -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,

View 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]

View File

@@ -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[

View File

@@ -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 = []

View File

@@ -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
}

View File

@@ -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

View 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)

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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')

View File

@@ -15,7 +15,7 @@ default_retrieval_model = {
'reranking_model_name': ''
},
'top_k': 2,
'score_threshold_enable': False
'score_threshold_enabled': False
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -53,4 +53,7 @@ OPENLLM_SERVER_URL=
LOCALAI_SERVER_URL=
# Cohere Credentials
COHERE_API_KEY=
COHERE_API_KEY=
# Jina Credentials
JINA_API_KEY=

View File

@@ -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

View 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)

View File

@@ -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"

View File

@@ -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.

View File

@@ -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:

View File

@@ -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>
)
}

View File

@@ -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"

View File

@@ -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')}

View File

@@ -138,6 +138,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
<>
<div
onClick={(e) => {
if (showSettingsModal)
return
e.preventDefault()
push(`/app/${app.id}/overview`)
}}

View File

@@ -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 } }

View File

@@ -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>
</>
}

View File

@@ -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>
)

View File

@@ -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>
)
}

View File

@@ -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;

View File

@@ -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}

View File

@@ -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>
)

View File

@@ -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} />

View File

@@ -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 contentrequired
- <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" />

View File

@@ -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" />

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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}

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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')}

View File

@@ -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>
)
}

View File

@@ -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')}
>

View File

@@ -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)',
}}

View File

@@ -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>
)

View File

@@ -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]'

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -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}'`}

View File

@@ -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>

View File

@@ -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}
</>

View 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

View 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

View File

@@ -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