From 5bb841935e124226dcdb94e4ec1d18a295ad768d Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Mon, 18 Dec 2023 16:25:37 +0800 Subject: [PATCH] feat: custom webapp logo (#1766) --- .../console/workspace/workspace.py | 63 ++++- api/controllers/console/wraps.py | 2 + api/controllers/files/image_preview.py | 23 ++ api/controllers/web/site.py | 11 + api/core/file/upload_file_parser.py | 2 +- ...8072f0caa04_add_custom_config_in_tenant.py | 32 +++ api/models/account.py | 11 + api/services/account_service.py | 6 + api/services/file_service.py | 22 +- api/services/workspace_service.py | 10 +- .../assets/vender/line/editor/colors.svg | 5 + .../communication/message-dots-circle.svg | 5 + .../assets/vender/solid/editor/colors.svg | 9 + .../icons/src/vender/line/editor/Colors.json | 39 +++ .../icons/src/vender/line/editor/Colors.tsx | 16 ++ .../icons/src/vender/line/editor/index.ts | 1 + .../communication/MessageDotsCircle.json | 38 +++ .../solid/communication/MessageDotsCircle.tsx | 16 ++ .../src/vender/solid/communication/index.ts | 1 + .../icons/src/vender/solid/editor/Colors.json | 62 +++++ .../icons/src/vender/solid/editor/Colors.tsx | 16 ++ .../icons/src/vender/solid/editor/index.ts | 1 + .../components/base/image-uploader/utils.ts | 6 +- .../custom/custom-app-header-brand/index.tsx | 70 ++++++ .../custom-app-header-brand/style.module.css | 3 + .../components/custom/custom-page/index.tsx | 52 ++++ .../custom/custom-web-app-brand/index.tsx | 234 ++++++++++++++++++ .../custom-web-app-brand/style.module.css | 3 + web/app/components/custom/style.module.css | 6 + .../header/account-setting/index.tsx | 11 + web/app/components/share/chat/index.tsx | 5 +- .../components/share/chat/welcome/index.tsx | 25 +- web/app/components/share/chatbot/index.tsx | 5 +- .../share/chatbot/welcome/index.tsx | 25 +- web/i18n/i18next-config.ts | 4 + web/i18n/lang/custom.en.ts | 30 +++ web/i18n/lang/custom.zh.ts | 30 +++ web/models/common.ts | 4 + web/service/base.ts | 4 +- web/service/common.ts | 4 + 40 files changed, 888 insertions(+), 24 deletions(-) create mode 100644 api/migrations/versions/88072f0caa04_add_custom_config_in_tenant.py create mode 100644 web/app/components/base/icons/assets/vender/line/editor/colors.svg create mode 100644 web/app/components/base/icons/assets/vender/solid/communication/message-dots-circle.svg create mode 100644 web/app/components/base/icons/assets/vender/solid/editor/colors.svg create mode 100644 web/app/components/base/icons/src/vender/line/editor/Colors.json create mode 100644 web/app/components/base/icons/src/vender/line/editor/Colors.tsx create mode 100644 web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.json create mode 100644 web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.tsx create mode 100644 web/app/components/base/icons/src/vender/solid/editor/Colors.json create mode 100644 web/app/components/base/icons/src/vender/solid/editor/Colors.tsx create mode 100644 web/app/components/custom/custom-app-header-brand/index.tsx create mode 100644 web/app/components/custom/custom-app-header-brand/style.module.css create mode 100644 web/app/components/custom/custom-page/index.tsx create mode 100644 web/app/components/custom/custom-web-app-brand/index.tsx create mode 100644 web/app/components/custom/custom-web-app-brand/style.module.css create mode 100644 web/app/components/custom/style.module.css create mode 100644 web/i18n/lang/custom.en.ts create mode 100644 web/i18n/lang/custom.zh.ts diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index 7630877119..2288ae21bd 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -10,12 +10,15 @@ from controllers.console import api from controllers.console.admin import admin_required from controllers.console.setup import setup_required from controllers.console.error import AccountNotLinkTenantError -from controllers.console.wraps import account_initialization_required +from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check +from controllers.console.datasets.error import NoFileUploadedError, TooManyFilesError, FileTooLargeError, UnsupportedFileTypeError from libs.helper import TimestampField from extensions.ext_database import db from models.account import Tenant +import services from services.account_service import TenantService from services.workspace_service import WorkspaceService +from services.file_service import FileService provider_fields = { 'provider_name': fields.String, @@ -34,6 +37,7 @@ tenant_fields = { 'providers': fields.List(fields.Nested(provider_fields)), 'in_trial': fields.Boolean, 'trial_end_reason': fields.String, + 'custom_config': fields.Raw(attribute='custom_config'), } tenants_fields = { @@ -130,6 +134,61 @@ class SwitchWorkspaceApi(Resource): new_tenant = db.session.query(Tenant).get(args['tenant_id']) # Get new tenant return {'result': 'success', 'new_tenant': marshal(WorkspaceService.get_tenant_info(new_tenant), tenant_fields)} + + +class CustomConfigWorkspaceApi(Resource): + @setup_required + @login_required + @account_initialization_required + @cloud_edition_billing_resource_check('workspace_custom') + def post(self): + parser = reqparse.RequestParser() + parser.add_argument('remove_webapp_brand', type=bool, location='json') + parser.add_argument('replace_webapp_logo', type=str, location='json') + args = parser.parse_args() + + custom_config_dict = { + 'remove_webapp_brand': args['remove_webapp_brand'], + 'replace_webapp_logo': args['replace_webapp_logo'], + } + + tenant = db.session.query(Tenant).filter(Tenant.id == current_user.current_tenant_id).one_or_404() + + tenant.custom_config_dict = custom_config_dict + db.session.commit() + + return {'result': 'success', 'tenant': marshal(WorkspaceService.get_tenant_info(tenant), tenant_fields)} + + +class WebappLogoWorkspaceApi(Resource): + @setup_required + @login_required + @account_initialization_required + @cloud_edition_billing_resource_check('workspace_custom') + def post(self): + # get file from request + file = request.files['file'] + + # check file + if 'file' not in request.files: + raise NoFileUploadedError() + + if len(request.files) > 1: + raise TooManyFilesError() + + extension = file.filename.split('.')[-1] + if extension.lower() not in ['svg', 'png']: + raise UnsupportedFileTypeError() + + try: + upload_file = FileService.upload_file(file, current_user, True) + + except services.errors.file.FileTooLargeError as file_too_large_error: + raise FileTooLargeError(file_too_large_error.description) + except services.errors.file.UnsupportedFileTypeError: + raise UnsupportedFileTypeError() + + return { 'id': upload_file.id }, 201 api.add_resource(TenantListApi, '/workspaces') # GET for getting all tenants @@ -137,3 +196,5 @@ api.add_resource(WorkspaceListApi, '/all-workspaces') # GET for getting all ten api.add_resource(TenantApi, '/workspaces/current', endpoint='workspaces_current') # GET for getting current tenant info api.add_resource(TenantApi, '/info', endpoint='info') # Deprecated api.add_resource(SwitchWorkspaceApi, '/workspaces/switch') # POST for switching tenant +api.add_resource(CustomConfigWorkspaceApi, '/workspaces/custom-config') +api.add_resource(WebappLogoWorkspaceApi, '/workspaces/custom-config/webapp-logo/upload') diff --git a/api/controllers/console/wraps.py b/api/controllers/console/wraps.py index f12802f1e4..3230067756 100644 --- a/api/controllers/console/wraps.py +++ b/api/controllers/console/wraps.py @@ -63,6 +63,8 @@ def cloud_edition_billing_resource_check(resource: str, abort(403, error_msg) elif resource == 'vector_space' and 0 < vector_space['limit'] <= vector_space['size']: abort(403, error_msg) + elif resource == 'workspace_custom' and not billing_info['can_replace_logo']: + abort(403, error_msg) elif resource == 'annotation' and 0 < annotation_quota_limit['limit'] <= annotation_quota_limit['size']: abort(403, error_msg) else: diff --git a/api/controllers/files/image_preview.py b/api/controllers/files/image_preview.py index 2f989ddcde..089f48f1f0 100644 --- a/api/controllers/files/image_preview.py +++ b/api/controllers/files/image_preview.py @@ -1,10 +1,12 @@ from flask import request, Response from flask_restful import Resource +from werkzeug.exceptions import NotFound import services from controllers.files import api from libs.exception import BaseHTTPException from services.file_service import FileService +from services.account_service import TenantService class ImagePreviewApi(Resource): @@ -29,9 +31,30 @@ class ImagePreviewApi(Resource): raise UnsupportedFileTypeError() return Response(generator, mimetype=mimetype) + + +class WorkspaceWebappLogoApi(Resource): + def get(self, workspace_id): + workspace_id = str(workspace_id) + + custom_config = TenantService.get_custom_config(workspace_id) + webapp_logo_file_id = custom_config.get('replace_webapp_logo') if custom_config is not None else None + + if not webapp_logo_file_id: + raise NotFound(f'webapp logo is not found') + + try: + generator, mimetype = FileService.get_public_image_preview( + webapp_logo_file_id, + ) + except services.errors.file.UnsupportedFileTypeError: + raise UnsupportedFileTypeError() + + return Response(generator, mimetype=mimetype) api.add_resource(ImagePreviewApi, '/files//image-preview') +api.add_resource(WorkspaceWebappLogoApi, '/files/workspaces//webapp-logo') class UnsupportedFileTypeError(BaseHTTPException): diff --git a/api/controllers/web/site.py b/api/controllers/web/site.py index 74849df7d1..4ca5ad05d9 100644 --- a/api/controllers/web/site.py +++ b/api/controllers/web/site.py @@ -2,6 +2,7 @@ import os from flask_restful import fields, marshal_with +from flask import current_app from werkzeug.exceptions import Forbidden from controllers.web import api @@ -43,6 +44,7 @@ class AppSiteApi(WebApiResource): 'model_config': fields.Nested(model_config_fields, allow_null=True), 'plan': fields.String, 'can_replace_logo': fields.Boolean, + 'custom_config': fields.Raw(attribute='custom_config'), } @marshal_with(app_fields) @@ -80,6 +82,15 @@ class AppSiteInfo: self.plan = tenant.plan self.can_replace_logo = can_replace_logo + if can_replace_logo: + base_url = current_app.config.get('FILES_URL') + remove_webapp_brand = tenant.custom_config_dict.get('remove_webapp_brand', False) + replace_webapp_logo = f'{base_url}/files/workspaces/{tenant.id}/webapp-logo' if tenant.custom_config_dict['replace_webapp_logo'] else None + self.custom_config = { + 'remove_webapp_brand': remove_webapp_brand, + 'replace_webapp_logo': replace_webapp_logo, + } + if app.enable_site and site.prompt_public: app_model_config = app.app_model_config self.model_config = app_model_config diff --git a/api/core/file/upload_file_parser.py b/api/core/file/upload_file_parser.py index 92335c297f..30125e0ea5 100644 --- a/api/core/file/upload_file_parser.py +++ b/api/core/file/upload_file_parser.py @@ -10,7 +10,7 @@ from flask import current_app from extensions.ext_storage import storage -SUPPORT_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'] +SUPPORT_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg'] class UploadFileParser: diff --git a/api/migrations/versions/88072f0caa04_add_custom_config_in_tenant.py b/api/migrations/versions/88072f0caa04_add_custom_config_in_tenant.py new file mode 100644 index 0000000000..6cf4d744d4 --- /dev/null +++ b/api/migrations/versions/88072f0caa04_add_custom_config_in_tenant.py @@ -0,0 +1,32 @@ +"""add custom config in tenant + +Revision ID: 88072f0caa04 +Revises: fca025d3b60f +Create Date: 2023-12-14 07:36:50.705362 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '88072f0caa04' +down_revision = '246ba09cbbdb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tenants', schema=None) as batch_op: + batch_op.add_column(sa.Column('custom_config', sa.Text(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tenants', schema=None) as batch_op: + batch_op.drop_column('custom_config') + + # ### end Alembic commands ### diff --git a/api/models/account.py b/api/models/account.py index 903fee4ea4..2e2c831ded 100644 --- a/api/models/account.py +++ b/api/models/account.py @@ -1,4 +1,6 @@ +import json import enum +from math import e from typing import List from flask_login import UserMixin @@ -112,6 +114,7 @@ class Tenant(db.Model): encrypt_public_key = db.Column(db.Text) plan = db.Column(db.String(255), nullable=False, server_default=db.text("'basic'::character varying")) status = db.Column(db.String(255), nullable=False, server_default=db.text("'normal'::character varying")) + custom_config = db.Column(db.Text) created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)')) @@ -121,6 +124,14 @@ class Tenant(db.Model): Account.id == TenantAccountJoin.account_id, TenantAccountJoin.tenant_id == self.id ).all() + + @property + def custom_config_dict(self) -> dict: + return json.loads(self.custom_config) if self.custom_config else None + + @custom_config_dict.setter + def custom_config_dict(self, value: dict): + self.custom_config = json.dumps(value) class TenantAccountJoinRole(enum.Enum): diff --git a/api/services/account_service.py b/api/services/account_service.py index c1f5dcdc1b..5c3ba5b6b1 100644 --- a/api/services/account_service.py +++ b/api/services/account_service.py @@ -412,6 +412,12 @@ class TenantService: db.session.delete(tenant) db.session.commit() + @staticmethod + def get_custom_config(tenant_id: str) -> None: + tenant = db.session.query(Tenant).filter(Tenant.id == tenant_id).one_or_404() + + return tenant.custom_config_dict + class RegisterService: diff --git a/api/services/file_service.py b/api/services/file_service.py index aaf82b57c8..78a35a9286 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -17,8 +17,8 @@ from models.model import UploadFile, EndUser from services.errors.file import FileTooLargeError, UnsupportedFileTypeError ALLOWED_EXTENSIONS = ['txt', 'markdown', 'md', 'pdf', 'html', 'htm', 'xlsx', 'docx', 'csv', - 'jpg', 'jpeg', 'png', 'webp', 'gif'] -IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'] + 'jpg', 'jpeg', 'png', 'webp', 'gif', 'svg'] +IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg'] PREVIEW_WORDS_LIMIT = 3000 @@ -154,3 +154,21 @@ class FileService: generator = storage.load(upload_file.key, stream=True) return generator, upload_file.mime_type + + @staticmethod + def get_public_image_preview(file_id: str) -> str: + upload_file = db.session.query(UploadFile) \ + .filter(UploadFile.id == file_id) \ + .first() + + if not upload_file: + raise NotFound("File not found or signature is invalid") + + # extract text from file + extension = upload_file.extension + if extension.lower() not in IMAGE_EXTENSIONS: + raise UnsupportedFileTypeError() + + generator = storage.load(upload_file.key) + + return generator, upload_file.mime_type diff --git a/api/services/workspace_service.py b/api/services/workspace_service.py index 975c311a3d..360eb25559 100644 --- a/api/services/workspace_service.py +++ b/api/services/workspace_service.py @@ -1,8 +1,11 @@ from flask_login import current_user from extensions.ext_database import db -from models.account import Tenant, TenantAccountJoin +from models.account import Tenant, TenantAccountJoin, TenantAccountJoinRole from models.provider import Provider +from services.billing_service import BillingService +from services.account_service import TenantService + class WorkspaceService: @classmethod @@ -28,6 +31,11 @@ class WorkspaceService: ).first() tenant_info['role'] = tenant_account_join.role + billing_info = BillingService.get_info(tenant_info['id']) + + if billing_info['can_replace_logo'] and TenantService.has_roles(tenant, [TenantAccountJoinRole.OWNER, TenantAccountJoinRole.ADMIN]): + tenant_info['custom_config'] = tenant.custom_config_dict + # Get providers providers = db.session.query(Provider).filter( Provider.tenant_id == tenant.id diff --git a/web/app/components/base/icons/assets/vender/line/editor/colors.svg b/web/app/components/base/icons/assets/vender/line/editor/colors.svg new file mode 100644 index 0000000000..685c175f72 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/editor/colors.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/vender/solid/communication/message-dots-circle.svg b/web/app/components/base/icons/assets/vender/solid/communication/message-dots-circle.svg new file mode 100644 index 0000000000..18ccff8a42 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/solid/communication/message-dots-circle.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/vender/solid/editor/colors.svg b/web/app/components/base/icons/assets/vender/solid/editor/colors.svg new file mode 100644 index 0000000000..b01556c219 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/solid/editor/colors.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/app/components/base/icons/src/vender/line/editor/Colors.json b/web/app/components/base/icons/src/vender/line/editor/Colors.json new file mode 100644 index 0000000000..baee8ee347 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Colors.json @@ -0,0 +1,39 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "24", + "height": "24", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "colors" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Icon", + "d": "M12 20.4722C13.0615 21.4223 14.4633 22 16 22C19.3137 22 22 19.3137 22 16C22 13.2331 20.1271 10.9036 17.5798 10.2102M6.42018 10.2102C3.87293 10.9036 2 13.2331 2 16C2 19.3137 4.68629 22 8 22C11.3137 22 14 19.3137 14 16C14 15.2195 13.851 14.4738 13.5798 13.7898M18 8C18 11.3137 15.3137 14 12 14C8.68629 14 6 11.3137 6 8C6 4.68629 8.68629 2 12 2C15.3137 2 18 4.68629 18 8Z", + "stroke": "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + } + ] + }, + "name": "Colors" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/editor/Colors.tsx b/web/app/components/base/icons/src/vender/line/editor/Colors.tsx new file mode 100644 index 0000000000..224ca116bd --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Colors.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Colors.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Colors' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/editor/index.ts b/web/app/components/base/icons/src/vender/line/editor/index.ts index 1725291ed9..6a80cab122 100644 --- a/web/app/components/base/icons/src/vender/line/editor/index.ts +++ b/web/app/components/base/icons/src/vender/line/editor/index.ts @@ -1,2 +1,3 @@ export { default as BezierCurve03 } from './BezierCurve03' +export { default as Colors } from './Colors' export { default as TypeSquare } from './TypeSquare' diff --git a/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.json b/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.json new file mode 100644 index 0000000000..dca92bf5d9 --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.json @@ -0,0 +1,38 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "24", + "height": "24", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "message-dots-circle" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Solid", + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M12 2C6.47715 2 2 6.47715 2 12C2 13.3283 2.25952 14.5985 2.73156 15.7608C2.77419 15.8658 2.79872 15.9264 2.81552 15.9711L2.82063 15.9849L2.82 15.9897C2.815 16.0266 2.80672 16.0769 2.79071 16.173L2.19294 19.7596C2.16612 19.9202 2.13611 20.0999 2.12433 20.256C2.11148 20.4261 2.10701 20.6969 2.22973 20.983C2.38144 21.3367 2.6633 21.6186 3.017 21.7703C3.30312 21.893 3.57386 21.8885 3.74404 21.8757C3.90013 21.8639 4.07985 21.8339 4.24049 21.8071L7.82705 21.2093C7.92309 21.1933 7.97339 21.185 8.0103 21.18L8.01505 21.1794L8.02887 21.1845C8.07362 21.2013 8.13423 21.2258 8.23921 21.2684C9.4015 21.7405 10.6717 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM6 12C6 11.1716 6.67157 10.5 7.5 10.5C8.32843 10.5 9 11.1716 9 12C9 12.8284 8.32843 13.5 7.5 13.5C6.67157 13.5 6 12.8284 6 12ZM10.5 12C10.5 11.1716 11.1716 10.5 12 10.5C12.8284 10.5 13.5 11.1716 13.5 12C13.5 12.8284 12.8284 13.5 12 13.5C11.1716 13.5 10.5 12.8284 10.5 12ZM16.5 10.5C15.6716 10.5 15 11.1716 15 12C15 12.8284 15.6716 13.5 16.5 13.5C17.3284 13.5 18 12.8284 18 12C18 11.1716 17.3284 10.5 16.5 10.5Z", + "fill": "currentColor" + }, + "children": [] + } + ] + } + ] + }, + "name": "MessageDotsCircle" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.tsx b/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.tsx new file mode 100644 index 0000000000..8cea978eea --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/communication/MessageDotsCircle.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './MessageDotsCircle.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'MessageDotsCircle' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/solid/communication/index.ts b/web/app/components/base/icons/src/vender/solid/communication/index.ts index 7e8eb36da9..58c4eaf186 100644 --- a/web/app/components/base/icons/src/vender/solid/communication/index.ts +++ b/web/app/components/base/icons/src/vender/solid/communication/index.ts @@ -1 +1,2 @@ +export { default as MessageDotsCircle } from './MessageDotsCircle' export { default as MessageFast } from './MessageFast' diff --git a/web/app/components/base/icons/src/vender/solid/editor/Colors.json b/web/app/components/base/icons/src/vender/solid/editor/Colors.json new file mode 100644 index 0000000000..6e5dc69049 --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/editor/Colors.json @@ -0,0 +1,62 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "24", + "height": "24", + "viewBox": "0 0 24 24", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "colors" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Solid" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M13.4494 13.2298C12.9854 13.3409 12.5002 13.3999 12 13.3999C10.2804 13.3999 8.72326 12.6997 7.59953 11.5677C6.4872 10.4471 5.8 8.90382 5.8 7.20007C5.8 3.77586 8.57584 1 12 1C15.4241 1 18.2 3.77586 18.2 7.20007C18.2 8.44569 17.8327 9.60551 17.2005 10.5771C16.3665 11.8588 15.0715 12.8131 13.5506 13.2047C13.517 13.2133 13.4833 13.2217 13.4494 13.2298Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M15.1476 14.7743C16.6646 14.1431 17.9513 13.0695 18.8465 11.7146C19.0004 11.4817 19.0773 11.3652 19.1762 11.3066C19.2615 11.2561 19.3659 11.2312 19.4648 11.2379C19.5795 11.2457 19.6773 11.3015 19.8728 11.4133C21.7413 12.4817 23 14.4946 23 16.7999C23 20.2241 20.2242 23 16.8 23C15.9123 23 15.0689 22.8139 14.3059 22.4782C14.0549 22.3678 13.9294 22.3126 13.8502 22.2049C13.7822 22.1126 13.7468 21.9922 13.7539 21.8777C13.7622 21.7444 13.8565 21.6018 14.045 21.3167C14.8373 20.1184 15.3234 18.6997 15.3917 17.1723C15.3969 17.0566 15.3996 16.9402 15.4 16.8233L15.4 16.7999C15.4 16.1888 15.333 15.5926 15.2057 15.0185C15.1876 14.9366 15.1682 14.8552 15.1476 14.7743Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M4.12723 11.4133C4.32273 11.3015 4.42049 11.2457 4.53516 11.2379C4.63414 11.2312 4.73848 11.2561 4.82382 11.3066C4.92269 11.3652 4.99964 11.4817 5.15355 11.7146C6.62074 13.9352 9.13929 15.4001 12 15.4001C12.4146 15.4001 12.822 15.3694 13.2201 15.31L13.2263 15.3357C13.3398 15.8045 13.4 16.2947 13.4 16.7999L13.4 16.8214C13.3997 16.9056 13.3977 16.9895 13.3941 17.0728C13.2513 20.3704 10.5327 23 7.2 23C3.77584 23 1 20.2241 1 16.7999C1 14.4946 2.25869 12.4817 4.12723 11.4133Z", + "fill": "currentColor" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "Colors" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/solid/editor/Colors.tsx b/web/app/components/base/icons/src/vender/solid/editor/Colors.tsx new file mode 100644 index 0000000000..224ca116bd --- /dev/null +++ b/web/app/components/base/icons/src/vender/solid/editor/Colors.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Colors.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Colors' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/solid/editor/index.ts b/web/app/components/base/icons/src/vender/solid/editor/index.ts index 7e9bd5d721..6b1a0a1afa 100644 --- a/web/app/components/base/icons/src/vender/solid/editor/index.ts +++ b/web/app/components/base/icons/src/vender/solid/editor/index.ts @@ -1,4 +1,5 @@ export { default as Brush01 } from './Brush01' export { default as Citations } from './Citations' +export { default as Colors } from './Colors' export { default as Paragraph } from './Paragraph' export { default as TypeSquare } from './TypeSquare' diff --git a/web/app/components/base/image-uploader/utils.ts b/web/app/components/base/image-uploader/utils.ts index 2d315693b5..0c1ada747d 100644 --- a/web/app/components/base/image-uploader/utils.ts +++ b/web/app/components/base/image-uploader/utils.ts @@ -6,13 +6,13 @@ type ImageUploadParams = { onSuccessCallback: (res: { id: string }) => void onErrorCallback: () => void } -type ImageUpload = (v: ImageUploadParams, isPublic?: boolean) => void +type ImageUpload = (v: ImageUploadParams, isPublic?: boolean, url?: string) => void export const imageUpload: ImageUpload = ({ file, onProgressCallback, onSuccessCallback, onErrorCallback, -}, isPublic) => { +}, isPublic, url) => { const formData = new FormData() formData.append('file', file) const onProgress = (e: ProgressEvent) => { @@ -26,7 +26,7 @@ export const imageUpload: ImageUpload = ({ xhr: new XMLHttpRequest(), data: formData, onprogress: onProgress, - }, isPublic) + }, isPublic, url) .then((res: { id: string }) => { onSuccessCallback(res) }) diff --git a/web/app/components/custom/custom-app-header-brand/index.tsx b/web/app/components/custom/custom-app-header-brand/index.tsx new file mode 100644 index 0000000000..2440d194e4 --- /dev/null +++ b/web/app/components/custom/custom-app-header-brand/index.tsx @@ -0,0 +1,70 @@ +import { useTranslation } from 'react-i18next' +import s from './style.module.css' +import Button from '@/app/components/base/button' +import { Grid01 } from '@/app/components/base/icons/src/vender/solid/layout' +import { Container, Database01 } from '@/app/components/base/icons/src/vender/line/development' +import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images' +import { useProviderContext } from '@/context/provider-context' +import { Plan } from '@/app/components/billing/type' + +const CustomAppHeaderBrand = () => { + const { t } = useTranslation() + const { plan } = useProviderContext() + + return ( +
+
{t('custom.app.title')}
+
+
+
+
+
+
YOUR LOGO
+
+
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+ +
+
{t('custom.app.changeLogoTip')}
+
+ ) +} + +export default CustomAppHeaderBrand diff --git a/web/app/components/custom/custom-app-header-brand/style.module.css b/web/app/components/custom/custom-app-header-brand/style.module.css new file mode 100644 index 0000000000..492733ff9f --- /dev/null +++ b/web/app/components/custom/custom-app-header-brand/style.module.css @@ -0,0 +1,3 @@ +.mask { + background: linear-gradient(95deg, rgba(255, 255, 255, 0.00) 43.9%, rgba(255, 255, 255, 0.80) 95.76%); ; +} \ No newline at end of file diff --git a/web/app/components/custom/custom-page/index.tsx b/web/app/components/custom/custom-page/index.tsx new file mode 100644 index 0000000000..44b5d0486b --- /dev/null +++ b/web/app/components/custom/custom-page/index.tsx @@ -0,0 +1,52 @@ +import { useTranslation } from 'react-i18next' +import CustomWebAppBrand from '../custom-web-app-brand' +import CustomAppHeaderBrand from '../custom-app-header-brand' +import s from '../style.module.css' +import GridMask from '@/app/components/base/grid-mask' +import UpgradeBtn from '@/app/components/billing/upgrade-btn' +import { useProviderContext } from '@/context/provider-context' +import { Plan } from '@/app/components/billing/type' +import { contactSalesUrl } from '@/app/components/billing/config' + +const CustomPage = () => { + const { t } = useTranslation() + const { plan } = useProviderContext() + + return ( +
+ { + plan.type === Plan.sandbox && ( + +
+
+
{t('custom.upgradeTip.prefix')}
+
{t('custom.upgradeTip.suffix')}
+
+ +
+
+ ) + } + + { + plan.type === Plan.sandbox && ( + <> +
+ + + ) + } + { + (plan.type === Plan.professional || plan.type === Plan.team) && ( +
+ {t('custom.customize.prefix')} + {t('custom.customize.contactUs')} + {t('custom.customize.suffix')} +
+ ) + } +
+ ) +} + +export default CustomPage diff --git a/web/app/components/custom/custom-web-app-brand/index.tsx b/web/app/components/custom/custom-web-app-brand/index.tsx new file mode 100644 index 0000000000..bbc0b4650a --- /dev/null +++ b/web/app/components/custom/custom-web-app-brand/index.tsx @@ -0,0 +1,234 @@ +import type { ChangeEvent } from 'react' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import s from './style.module.css' +import LogoSite from '@/app/components/base/logo/logo-site' +import Switch from '@/app/components/base/switch' +import Button from '@/app/components/base/button' +import { Loading02 } from '@/app/components/base/icons/src/vender/line/general' +import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication' +import { ImagePlus } from '@/app/components/base/icons/src/vender/line/images' +import { useProviderContext } from '@/context/provider-context' +import { Plan } from '@/app/components/billing/type' +import { imageUpload } from '@/app/components/base/image-uploader/utils' +import type {} from '@/app/components/base/image-uploader/utils' +import { useToastContext } from '@/app/components/base/toast' +import { + updateCurrentWorkspace, +} from '@/service/common' +import { useAppContext } from '@/context/app-context' +import { API_PREFIX } from '@/config' + +const ALLOW_FILE_EXTENSIONS = ['svg', 'png'] + +const CustomWebAppBrand = () => { + const { t } = useTranslation() + const { notify } = useToastContext() + const { plan } = useProviderContext() + const { + currentWorkspace, + mutateCurrentWorkspace, + isCurrentWorkspaceManager, + } = useAppContext() + const [fileId, setFileId] = useState('') + const [uploadProgress, setUploadProgress] = useState(0) + const isSandbox = plan.type === Plan.sandbox + const uploading = uploadProgress > 0 && uploadProgress < 100 + const webappLogo = currentWorkspace.custom_config?.replace_webapp_logo || '' + const webappBrandRemoved = currentWorkspace.custom_config?.remove_webapp_brand + + const handleChange = (e: ChangeEvent) => { + const file = e.target.files?.[0] + + if (!file) + return + + if (file.size > 5 * 1024 * 1024) { + notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: 5 }) }) + return + } + + imageUpload({ + file, + onProgressCallback: (progress) => { + setUploadProgress(progress) + }, + onSuccessCallback: (res) => { + setUploadProgress(100) + setFileId(res.id) + }, + onErrorCallback: () => { + notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') }) + setUploadProgress(-1) + }, + }, false, '/workspaces/custom-config/webapp-logo/upload') + } + + const handleApply = async () => { + await updateCurrentWorkspace({ + url: '/workspaces/custom-config', + body: { + remove_webapp_brand: webappBrandRemoved, + replace_webapp_logo: fileId, + }, + }) + mutateCurrentWorkspace() + setFileId('') + } + + const handleRestore = async () => { + await updateCurrentWorkspace({ + url: '/workspaces/custom-config', + body: { + remove_webapp_brand: false, + replace_webapp_logo: null, + }, + }) + mutateCurrentWorkspace() + } + + const handleSwitch = async (checked: boolean) => { + await updateCurrentWorkspace({ + url: '/workspaces/custom-config', + body: { + remove_webapp_brand: checked, + replace_webapp_logo: webappLogo, + }, + }) + mutateCurrentWorkspace() + } + + const handleCancel = () => { + setFileId('') + setUploadProgress(0) + } + + return ( +
+
{t('custom.webapp.title')}
+
+
+
+
+ +
+
+
+
+
+ { + !webappBrandRemoved && ( +
+ POWERED BY + { + webappLogo + ? logo + : + } +
+ ) + } +
+
+
+ {t('custom.webapp.removeBrand')} + +
+
+
+
{t('custom.webapp.changeLogo')}
+
{t('custom.webapp.changeLogoTip')}
+
+
+ { + !uploading && ( + + ) + } + { + uploading && ( + + ) + } + { + fileId && ( + <> + + + + ) + } +
+ +
+
+ { + uploadProgress === -1 && ( +
{t('custom.uploadedFail')}
+ ) + } +
+ ) +} + +export default CustomWebAppBrand diff --git a/web/app/components/custom/custom-web-app-brand/style.module.css b/web/app/components/custom/custom-web-app-brand/style.module.css new file mode 100644 index 0000000000..6fe7d84944 --- /dev/null +++ b/web/app/components/custom/custom-web-app-brand/style.module.css @@ -0,0 +1,3 @@ +.mask { + background: linear-gradient(273deg, rgba(255, 255, 255, 0.00) 51.75%, rgba(255, 255, 255, 0.80) 115.32%); +} \ No newline at end of file diff --git a/web/app/components/custom/style.module.css b/web/app/components/custom/style.module.css new file mode 100644 index 0000000000..088fd3a55d --- /dev/null +++ b/web/app/components/custom/style.module.css @@ -0,0 +1,6 @@ +.textGradient { + background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} \ No newline at end of file diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx index cdbb789e2e..34e5d878ae 100644 --- a/web/app/components/header/account-setting/index.tsx +++ b/web/app/components/header/account-setting/index.tsx @@ -14,6 +14,7 @@ import DataSourcePage from './data-source-page' import ModelPage from './model-page' import s from './index.module.css' import BillingPage from '@/app/components/billing/billing-page' +import CustomPage from '@/app/components/custom/custom-page' import Modal from '@/app/components/base/modal' import { Database03, @@ -26,8 +27,11 @@ import { User01 as User01Solid, Users01 as Users01Solid } from '@/app/components import { Globe01 } from '@/app/components/base/icons/src/vender/line/mapsAndTravel' import { AtSign, XClose } from '@/app/components/base/icons/src/vender/line/general' import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes' +import { Colors } from '@/app/components/base/icons/src/vender/line/editor' +import { Colors as ColorsSolid } from '@/app/components/base/icons/src/vender/solid/editor' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { useProviderContext } from '@/context/provider-context' +import { IS_CE_EDITION } from '@/config' const iconClassName = ` w-4 h-4 ml-3 mr-2 @@ -96,6 +100,12 @@ export default function AccountSetting({ icon: , activeIcon: , }, + { + key: IS_CE_EDITION ? false : 'custom', + name: t('custom.custom'), + icon: , + activeIcon: , + }, ].filter(item => !!item.key) as GroupItem[] })() @@ -206,6 +216,7 @@ export default function AccountSetting({ {activeMenu === 'data-source' && } {activeMenu === 'plugin' && } {activeMenu === 'api-based-extension' && } + {activeMenu === 'custom' && }
diff --git a/web/app/components/share/chat/index.tsx b/web/app/components/share/chat/index.tsx index fa9142cb40..7317d9defd 100644 --- a/web/app/components/share/chat/index.tsx +++ b/web/app/components/share/chat/index.tsx @@ -71,6 +71,7 @@ const Main: FC = ({ const [inited, setInited] = useState(false) const [plan, setPlan] = useState('basic') // basic/plus/pro const [canReplaceLogo, setCanReplaceLogo] = useState(false) + const [customConfig, setCustomConfig] = useState(null) // in mobile, show sidebar by click button const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. @@ -364,10 +365,11 @@ const Main: FC = ({ (async () => { try { const [appData, conversationData, appParams]: any = await fetchInitData() - const { app_id: appId, site: siteInfo, plan, can_replace_logo }: any = appData + const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData setAppId(appId) setPlan(plan) setCanReplaceLogo(can_replace_logo) + setCustomConfig(custom_config) const tempIsPublicVersion = siteInfo.prompt_public setIsPublicVersion(tempIsPublicVersion) const prompt_template = '' @@ -752,6 +754,7 @@ const Main: FC = ({ onInputsChange={setCurrInputs} plan={plan} canReplaceLogo={canReplaceLogo} + customConfig={customConfig} > { diff --git a/web/app/components/share/chat/welcome/index.tsx b/web/app/components/share/chat/welcome/index.tsx index b8e533daa0..65c0090e6f 100644 --- a/web/app/components/share/chat/welcome/index.tsx +++ b/web/app/components/share/chat/welcome/index.tsx @@ -27,6 +27,10 @@ export type IWelcomeProps = { onInputsChange: (inputs: Record) => void plan?: string canReplaceLogo?: boolean + customConfig?: { + remove_webapp_brand?: boolean + replace_webapp_logo?: string + } } const Welcome: FC = ({ @@ -34,13 +38,12 @@ const Welcome: FC = ({ hasSetInputs, isPublicVersion, siteInfo, - plan, promptConfig, onStartChat, canEidtInpus, savedInputs, onInputsChange, - canReplaceLogo, + customConfig, }) => { const { t } = useTranslation() const hasVar = promptConfig.prompt_variables.length > 0 @@ -352,10 +355,20 @@ const Welcome: FC = ({ :
} - {!canReplaceLogo && - {t('share.chat.powerBy')} - - } + { + customConfig?.remove_webapp_brand + ? null + : ( + + {t('share.chat.powerBy')} + { + customConfig?.replace_webapp_logo + ? logo + : + } + + ) + } )} diff --git a/web/app/components/share/chatbot/index.tsx b/web/app/components/share/chatbot/index.tsx index e0ac6b86c3..3a93c2b5bf 100644 --- a/web/app/components/share/chatbot/index.tsx +++ b/web/app/components/share/chatbot/index.tsx @@ -55,6 +55,7 @@ const Main: FC = ({ const [inited, setInited] = useState(false) const [plan, setPlan] = useState('basic') // basic/plus/pro const [canReplaceLogo, setCanReplaceLogo] = useState(false) + const [customConfig, setCustomConfig] = useState(null) // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. useEffect(() => { if (siteInfo?.title) { @@ -283,10 +284,11 @@ const Main: FC = ({ (async () => { try { const [appData, conversationData, appParams]: any = await fetchInitData() - const { app_id: appId, site: siteInfo, plan, can_replace_logo }: any = appData + const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData setAppId(appId) setPlan(plan) setCanReplaceLogo(can_replace_logo) + setCustomConfig(custom_config) const tempIsPublicVersion = siteInfo.prompt_public setIsPublicVersion(tempIsPublicVersion) const prompt_template = '' @@ -592,6 +594,7 @@ const Main: FC = ({ onInputsChange={setCurrInputs} plan={plan} canReplaceLogo={canReplaceLogo} + customConfig={customConfig} > { shouldReload && ( diff --git a/web/app/components/share/chatbot/welcome/index.tsx b/web/app/components/share/chatbot/welcome/index.tsx index c8e9e4bb7e..93599d90c4 100644 --- a/web/app/components/share/chatbot/welcome/index.tsx +++ b/web/app/components/share/chatbot/welcome/index.tsx @@ -27,6 +27,10 @@ export type IWelcomeProps = { onInputsChange: (inputs: Record) => void plan: string canReplaceLogo?: boolean + customConfig?: { + remove_webapp_brand?: boolean + replace_webapp_logo?: string + } } const Welcome: FC = ({ @@ -34,13 +38,12 @@ const Welcome: FC = ({ hasSetInputs, isPublicVersion, siteInfo, - plan, promptConfig, onStartChat, canEditInputs, savedInputs, onInputsChange, - canReplaceLogo, + customConfig, }) => { const { t } = useTranslation() const hasVar = promptConfig.prompt_variables.length > 0 @@ -353,10 +356,20 @@ const Welcome: FC = ({ :
} - {!canReplaceLogo && - {t('share.chat.powerBy')} - - } + { + customConfig?.remove_webapp_brand + ? null + : ( + + {t('share.chat.powerBy')} + { + customConfig?.replace_webapp_logo + ? logo + : + } + + ) + } )} diff --git a/web/i18n/i18next-config.ts b/web/i18n/i18next-config.ts index ac121cc1e3..cbc79016c2 100644 --- a/web/i18n/i18next-config.ts +++ b/web/i18n/i18next-config.ts @@ -37,6 +37,8 @@ import exploreEn from './lang/explore.en' import exploreZh from './lang/explore.zh' import billingEn from './lang/billing.en' import billingZh from './lang/billing.zh' +import customEn from './lang/custom.en' +import customZh from './lang/custom.zh' const resources = { 'en': { @@ -62,6 +64,7 @@ const resources = { explore: exploreEn, // billing billing: billingEn, + custom: customEn, }, }, 'zh-Hans': { @@ -86,6 +89,7 @@ const resources = { datasetCreation: datasetCreationZh, explore: exploreZh, billing: billingZh, + custom: customZh, }, }, } diff --git a/web/i18n/lang/custom.en.ts b/web/i18n/lang/custom.en.ts new file mode 100644 index 0000000000..5e7f8bbf96 --- /dev/null +++ b/web/i18n/lang/custom.en.ts @@ -0,0 +1,30 @@ +const translation = { + custom: 'Customization', + upgradeTip: { + prefix: 'Upgrade your plan to', + suffix: 'customize your brand.', + }, + webapp: { + title: 'Customize web app brand', + removeBrand: 'Remove Powered by Dify', + changeLogo: 'Change Powered by Brand Image', + changeLogoTip: 'SVG or PNG format with a minimum size of 40x40px', + }, + app: { + title: 'Customize app header brand', + changeLogoTip: 'SVG or PNG format with a minimum size of 80x80px', + }, + upload: 'Upload', + uploading: 'Uploading', + uploadedFail: 'Image upload failed, please re-upload.', + change: 'Change', + apply: 'Apply', + restore: 'Restore Defaults', + customize: { + contactUs: ' contact us ', + prefix: 'To customize the brand logo within the app, please', + suffix: 'to upgrade to the Enterprise edition.', + }, +} + +export default translation diff --git a/web/i18n/lang/custom.zh.ts b/web/i18n/lang/custom.zh.ts new file mode 100644 index 0000000000..9910e0fa61 --- /dev/null +++ b/web/i18n/lang/custom.zh.ts @@ -0,0 +1,30 @@ +const translation = { + custom: '定制', + upgradeTip: { + prefix: '升级您的计划以', + suffix: '定制您的品牌。', + }, + webapp: { + title: '定制 web app 品牌', + removeBrand: '移除 Powered by Dify', + changeLogo: '更改 Powered by Brand 图片', + changeLogoTip: 'SVG 或 PNG 格式,最小尺寸为 40x40px', + }, + app: { + title: '定制应用品牌', + changeLogoTip: 'SVG 或 PNG 格式,最小尺寸为 80x80px', + }, + upload: '上传', + uploading: '上传中', + uploadedFail: '图片上传失败,请重新上传。', + change: '更改', + apply: '应用', + restore: '恢复默认', + customize: { + contactUs: '联系我们', + prefix: '如需在 Dify 内自定义品牌图标,请', + suffix: '升级至企业版。', + }, +} + +export default translation diff --git a/web/models/common.ts b/web/models/common.ts index 3f7bddf160..58e1ff101a 100644 --- a/web/models/common.ts +++ b/web/models/common.ts @@ -123,6 +123,10 @@ export type ICurrentWorkspace = Omit & { providers: Provider[] in_trail: boolean trial_end_reason?: string + custom_config?: { + remove_webapp_brand?: boolean + replace_webapp_logo?: string + } } export type DataSourceNotionPage = { diff --git a/web/service/base.ts b/web/service/base.ts index 60882dc4b9..1735180d20 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -297,7 +297,7 @@ const baseFetch = ( ]) as Promise } -export const upload = (options: any, isPublicAPI?: boolean): Promise => { +export const upload = (options: any, isPublicAPI?: boolean, url?: string): Promise => { const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX let token = '' if (isPublicAPI) { @@ -318,7 +318,7 @@ export const upload = (options: any, isPublicAPI?: boolean): Promise => { } const defaultOptions = { method: 'POST', - url: `${urlPrefix}/files/upload`, + url: url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`, headers: { Authorization: `Bearer ${token}`, }, diff --git a/web/service/common.ts b/web/service/common.ts index e6c4e65a71..f7c4eb197d 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -103,6 +103,10 @@ export const fetchCurrentWorkspace: Fetcher(url, { params }) } +export const updateCurrentWorkspace: Fetcher }> = ({ url, body }) => { + return post(url, { body }) +} + export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record }> = ({ url, params }) => { return get<{ workspaces: IWorkspace[] }>(url, { params }) }