mirror of
https://gitee.com/dify_ai/dify.git
synced 2024-11-30 02:08:37 +08:00
feat: custom webapp logo (#1766)
This commit is contained in:
parent
65fd4b39ce
commit
5bb841935e
@ -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')
|
||||
|
@ -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:
|
||||
|
@ -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/<uuid:file_id>/image-preview')
|
||||
api.add_resource(WorkspaceWebappLogoApi, '/files/workspaces/<uuid:workspace_id>/webapp-logo')
|
||||
|
||||
|
||||
class UnsupportedFileTypeError(BaseHTTPException):
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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 ###
|
@ -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):
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="colors">
|
||||
<path 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="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 594 B |
@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="message-dots-circle">
|
||||
<path 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="black"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1,9 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="colors">
|
||||
<g id="Solid">
|
||||
<path 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="black"/>
|
||||
<path 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="black"/>
|
||||
<path 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="black"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
@ -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"
|
||||
}
|
@ -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<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Colors'
|
||||
|
||||
export default Icon
|
@ -1,2 +1,3 @@
|
||||
export { default as BezierCurve03 } from './BezierCurve03'
|
||||
export { default as Colors } from './Colors'
|
||||
export { default as TypeSquare } from './TypeSquare'
|
||||
|
@ -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"
|
||||
}
|
@ -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<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'MessageDotsCircle'
|
||||
|
||||
export default Icon
|
@ -1 +1,2 @@
|
||||
export { default as MessageDotsCircle } from './MessageDotsCircle'
|
||||
export { default as MessageFast } from './MessageFast'
|
||||
|
@ -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"
|
||||
}
|
@ -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<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Colors'
|
||||
|
||||
export default Icon
|
@ -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'
|
||||
|
@ -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)
|
||||
})
|
||||
|
70
web/app/components/custom/custom-app-header-brand/index.tsx
Normal file
70
web/app/components/custom/custom-app-header-brand/index.tsx
Normal file
@ -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 (
|
||||
<div className='py-3'>
|
||||
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.app.title')}</div>
|
||||
<div className='relative mb-4 rounded-xl bg-gray-100 border-[0.5px] border-black/[0.08] shadow-xs'>
|
||||
<div className={`${s.mask} absolute inset-0 rounded-xl`}></div>
|
||||
<div className='flex items-center pl-5 h-14 rounded-t-xl'>
|
||||
<div className='relative flex items-center mr-[199px] w-[120px] h-10 bg-[rgba(217,45,32,0.12)]'>
|
||||
<div className='ml-[1px] mr-[3px] w-[34px] h-[34px] border-8 border-black/[0.16] rounded-full'></div>
|
||||
<div className='text-[13px] font-bold text-black/[0.24]'>YOUR LOGO</div>
|
||||
<div className='absolute top-0 bottom-0 left-0.5 w-[0.5px] bg-[#F97066] opacity-50'></div>
|
||||
<div className='absolute top-0 bottom-0 right-0.5 w-[0.5px] bg-[#F97066] opacity-50'></div>
|
||||
<div className='absolute left-0 right-0 top-0.5 h-[0.5px] bg-[#F97066] opacity-50'></div>
|
||||
<div className='absolute left-0 right-0 bottom-0.5 h-[0.5px] bg-[#F97066] opacity-50'></div>
|
||||
</div>
|
||||
<div className='flex items-center mr-3 px-3 h-7 rounded-xl bg-white shadow-xs'>
|
||||
<Grid01 className='shrink-0 mr-2 w-4 h-4 text-[#155eef]' />
|
||||
<div className='w-12 h-1.5 rounded-[5px] bg-[#155eef] opacity-80'></div>
|
||||
</div>
|
||||
<div className='flex items-center mr-3 px-3 h-7'>
|
||||
<Container className='shrink-0 mr-2 w-4 h-4 text-gray-500' />
|
||||
<div className='w-[50px] h-1.5 rounded-[5px] bg-gray-300'></div>
|
||||
</div>
|
||||
<div className='flex items-center px-3 h-7'>
|
||||
<Database01 className='shrink-0 mr-2 w-4 h-4 text-gray-500' />
|
||||
<div className='w-14 h-1.5 rounded-[5px] bg-gray-300 opacity-80'></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-8 border-t border-t-gray-200 rounded-b-xl'></div>
|
||||
</div>
|
||||
<div className='flex items-center mb-2'>
|
||||
<Button
|
||||
className={`
|
||||
!h-8 !px-3 bg-white !text-[13px]
|
||||
${plan.type === Plan.sandbox ? 'opacity-40' : ''}
|
||||
`}
|
||||
disabled={plan.type === Plan.sandbox}
|
||||
>
|
||||
<ImagePlus className='mr-2 w-4 h-4' />
|
||||
{t('custom.upload')}
|
||||
</Button>
|
||||
<div className='mx-2 h-5 w-[1px] bg-black/5'></div>
|
||||
<Button
|
||||
className={`
|
||||
!h-8 !px-3 bg-white !text-[13px]
|
||||
${plan.type === Plan.sandbox ? 'opacity-40' : ''}
|
||||
`}
|
||||
disabled={plan.type === Plan.sandbox}
|
||||
>
|
||||
{t('custom.restore')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{t('custom.app.changeLogoTip')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomAppHeaderBrand
|
@ -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%); ;
|
||||
}
|
52
web/app/components/custom/custom-page/index.tsx
Normal file
52
web/app/components/custom/custom-page/index.tsx
Normal file
@ -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 (
|
||||
<div className='flex flex-col'>
|
||||
{
|
||||
plan.type === Plan.sandbox && (
|
||||
<GridMask canvasClassName='!rounded-xl'>
|
||||
<div className='flex justify-between mb-1 px-6 py-5 h-[88px] shadow-md rounded-xl border-[0.5px] border-gray-200'>
|
||||
<div className={`${s.textGradient} leading-[24px] text-base font-semibold`}>
|
||||
<div>{t('custom.upgradeTip.prefix')}</div>
|
||||
<div>{t('custom.upgradeTip.suffix')}</div>
|
||||
</div>
|
||||
<UpgradeBtn />
|
||||
</div>
|
||||
</GridMask>
|
||||
)
|
||||
}
|
||||
<CustomWebAppBrand />
|
||||
{
|
||||
plan.type === Plan.sandbox && (
|
||||
<>
|
||||
<div className='my-2 h-[0.5px] bg-gray-100'></div>
|
||||
<CustomAppHeaderBrand />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
(plan.type === Plan.professional || plan.type === Plan.team) && (
|
||||
<div className='absolute bottom-0 h-[50px] leading-[50px] text-xs text-gray-500'>
|
||||
{t('custom.customize.prefix')}
|
||||
<a className='text-[#155EEF]' href={contactSalesUrl} target='_blank'>{t('custom.customize.contactUs')}</a>
|
||||
{t('custom.customize.suffix')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomPage
|
234
web/app/components/custom/custom-web-app-brand/index.tsx
Normal file
234
web/app/components/custom/custom-web-app-brand/index.tsx
Normal file
@ -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<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className='py-4'>
|
||||
<div className='mb-2 text-sm font-medium text-gray-900'>{t('custom.webapp.title')}</div>
|
||||
<div className='relative mb-4 pl-4 pb-6 pr-[119px] rounded-xl border-[0.5px] border-black/[0.08] shadow-xs bg-gray-50 overflow-hidden'>
|
||||
<div className={`${s.mask} absolute top-0 left-0 w-full -bottom-2 z-10`}></div>
|
||||
<div className='flex items-center -mt-2 mb-4 p-6 bg-white rounded-xl'>
|
||||
<div className='flex items-center px-4 w-[125px] h-9 rounded-lg bg-primary-600 border-[0.5px] border-primary-700 shadow-xs'>
|
||||
<MessageDotsCircle className='shrink-0 mr-2 w-4 h-4 text-white' />
|
||||
<div className='grow h-2 rounded-sm bg-white opacity-50' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center h-5 justify-between'>
|
||||
<div className='w-[369px] h-1.5 rounded-sm bg-gray-200 opacity-80' />
|
||||
{
|
||||
!webappBrandRemoved && (
|
||||
<div className='flex items-center text-[10px] font-medium text-gray-400'>
|
||||
POWERED BY
|
||||
{
|
||||
webappLogo
|
||||
? <img key={webappLogo} src={`${API_PREFIX.slice(0, -12)}/files/workspaces/${currentWorkspace.id}/webapp-logo`} alt='logo' className='ml-2 block w-auto h-5' />
|
||||
: <LogoSite className='ml-2 !h-5' />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-between mb-2 px-4 h-14 rounded-xl border-[0.5px] border-gray-200 bg-gray-50 text-sm font-medium text-gray-900'>
|
||||
{t('custom.webapp.removeBrand')}
|
||||
<Switch
|
||||
size='l'
|
||||
defaultValue={webappBrandRemoved}
|
||||
disabled={isSandbox || !isCurrentWorkspaceManager}
|
||||
onChange={handleSwitch}
|
||||
/>
|
||||
</div>
|
||||
<div className={`
|
||||
flex items-center justify-between px-4 py-3 rounded-xl border-[0.5px] border-gray-200 bg-gray-50
|
||||
${webappBrandRemoved && 'opacity-30'}
|
||||
`}>
|
||||
<div>
|
||||
<div className='leading-5 text-sm font-medium text-gray-900'>{t('custom.webapp.changeLogo')}</div>
|
||||
<div className='leading-[18px] text-xs text-gray-500'>{t('custom.webapp.changeLogoTip')}</div>
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
!uploading && (
|
||||
<Button
|
||||
className={`
|
||||
relative mr-2 !h-8 !px-3 bg-white !text-[13px]
|
||||
${isSandbox ? 'opacity-40' : ''}
|
||||
`}
|
||||
disabled={isSandbox || webappBrandRemoved || !isCurrentWorkspaceManager}
|
||||
>
|
||||
<ImagePlus className='mr-2 w-4 h-4' />
|
||||
{
|
||||
(webappLogo || fileId)
|
||||
? t('custom.change')
|
||||
: t('custom.upload')
|
||||
}
|
||||
<input
|
||||
className={`
|
||||
absolute block inset-0 opacity-0 text-[0] w-full
|
||||
${(isSandbox || webappBrandRemoved) ? 'cursor-not-allowed' : 'cursor-pointer'}
|
||||
`}
|
||||
onClick={e => (e.target as HTMLInputElement).value = ''}
|
||||
type='file'
|
||||
accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')}
|
||||
onChange={handleChange}
|
||||
disabled={isSandbox || webappBrandRemoved || !isCurrentWorkspaceManager}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
uploading && (
|
||||
<Button
|
||||
className='relative mr-2 !h-8 !px-3 bg-white !text-[13px] opacity-40'
|
||||
disabled={true}
|
||||
>
|
||||
<Loading02 className='animate-spin mr-2 w-4 h-4' />
|
||||
{t('custom.uploading')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
fileId && (
|
||||
<>
|
||||
<Button
|
||||
type='primary'
|
||||
className='mr-2 !h-8 !px-3 !py-0 !text-[13px]'
|
||||
onClick={handleApply}
|
||||
disabled={webappBrandRemoved || !isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('custom.apply')}
|
||||
</Button>
|
||||
<Button
|
||||
className='mr-2 !h-8 !px-3 !text-[13px] bg-white'
|
||||
onClick={handleCancel}
|
||||
disabled={webappBrandRemoved || !isCurrentWorkspaceManager}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='mr-2 h-5 w-[1px] bg-black/5'></div>
|
||||
<Button
|
||||
className={`
|
||||
!h-8 !px-3 bg-white !text-[13px]
|
||||
${isSandbox ? 'opacity-40' : ''}
|
||||
`}
|
||||
disabled={isSandbox || (!webappLogo && !webappBrandRemoved) || webappBrandRemoved || !isCurrentWorkspaceManager}
|
||||
onClick={handleRestore}
|
||||
>
|
||||
{t('custom.restore')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
uploadProgress === -1 && (
|
||||
<div className='mt-2 text-xs text-[#D92D20]'>{t('custom.uploadedFail')}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomWebAppBrand
|
@ -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%);
|
||||
}
|
6
web/app/components/custom/style.module.css
Normal file
6
web/app/components/custom/style.module.css
Normal file
@ -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;
|
||||
}
|
@ -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: <Webhooks className={iconClassName} />,
|
||||
activeIcon: <Webhooks className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: IS_CE_EDITION ? false : 'custom',
|
||||
name: t('custom.custom'),
|
||||
icon: <Colors className={iconClassName} />,
|
||||
activeIcon: <ColorsSolid className={iconClassName} />,
|
||||
},
|
||||
].filter(item => !!item.key) as GroupItem[]
|
||||
})()
|
||||
|
||||
@ -206,6 +216,7 @@ export default function AccountSetting({
|
||||
{activeMenu === 'data-source' && <DataSourcePage />}
|
||||
{activeMenu === 'plugin' && <PluginPage />}
|
||||
{activeMenu === 'api-based-extension' && <ApiBasedExtensionPage /> }
|
||||
{activeMenu === 'custom' && <CustomPage /> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -71,6 +71,7 @@ const Main: FC<IMainProps> = ({
|
||||
const [inited, setInited] = useState<boolean>(false)
|
||||
const [plan, setPlan] = useState<string>('basic') // basic/plus/pro
|
||||
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
|
||||
const [customConfig, setCustomConfig] = useState<any>(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<IMainProps> = ({
|
||||
(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<IMainProps> = ({
|
||||
onInputsChange={setCurrInputs}
|
||||
plan={plan}
|
||||
canReplaceLogo={canReplaceLogo}
|
||||
customConfig={customConfig}
|
||||
></ConfigSence>
|
||||
|
||||
{
|
||||
|
@ -27,6 +27,10 @@ export type IWelcomeProps = {
|
||||
onInputsChange: (inputs: Record<string, any>) => void
|
||||
plan?: string
|
||||
canReplaceLogo?: boolean
|
||||
customConfig?: {
|
||||
remove_webapp_brand?: boolean
|
||||
replace_webapp_logo?: string
|
||||
}
|
||||
}
|
||||
|
||||
const Welcome: FC<IWelcomeProps> = ({
|
||||
@ -34,13 +38,12 @@ const Welcome: FC<IWelcomeProps> = ({
|
||||
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<IWelcomeProps> = ({
|
||||
</div>
|
||||
: <div>
|
||||
</div>}
|
||||
{!canReplaceLogo && <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
|
||||
<span className='uppercase'>{t('share.chat.powerBy')}</span>
|
||||
<FootLogo />
|
||||
</a>}
|
||||
{
|
||||
customConfig?.remove_webapp_brand
|
||||
? null
|
||||
: (
|
||||
<a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
|
||||
<span className='uppercase'>{t('share.chat.powerBy')}</span>
|
||||
{
|
||||
customConfig?.replace_webapp_logo
|
||||
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
: <FootLogo />
|
||||
}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -55,6 +55,7 @@ const Main: FC<IMainProps> = ({
|
||||
const [inited, setInited] = useState<boolean>(false)
|
||||
const [plan, setPlan] = useState<string>('basic') // basic/plus/pro
|
||||
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
|
||||
const [customConfig, setCustomConfig] = useState<any>(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<IMainProps> = ({
|
||||
(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<IMainProps> = ({
|
||||
onInputsChange={setCurrInputs}
|
||||
plan={plan}
|
||||
canReplaceLogo={canReplaceLogo}
|
||||
customConfig={customConfig}
|
||||
></ConfigScene>
|
||||
{
|
||||
shouldReload && (
|
||||
|
@ -27,6 +27,10 @@ export type IWelcomeProps = {
|
||||
onInputsChange: (inputs: Record<string, any>) => void
|
||||
plan: string
|
||||
canReplaceLogo?: boolean
|
||||
customConfig?: {
|
||||
remove_webapp_brand?: boolean
|
||||
replace_webapp_logo?: string
|
||||
}
|
||||
}
|
||||
|
||||
const Welcome: FC<IWelcomeProps> = ({
|
||||
@ -34,13 +38,12 @@ const Welcome: FC<IWelcomeProps> = ({
|
||||
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<IWelcomeProps> = ({
|
||||
</div>
|
||||
: <div>
|
||||
</div>}
|
||||
{!canReplaceLogo && <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
|
||||
<span className='uppercase'>{t('share.chat.powerBy')}</span>
|
||||
<FootLogo />
|
||||
</a>}
|
||||
{
|
||||
customConfig?.remove_webapp_brand
|
||||
? null
|
||||
: (
|
||||
<a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
|
||||
<span className='uppercase'>{t('share.chat.powerBy')}</span>
|
||||
{
|
||||
customConfig?.replace_webapp_logo
|
||||
? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
|
||||
: <FootLogo />
|
||||
}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
30
web/i18n/lang/custom.en.ts
Normal file
30
web/i18n/lang/custom.en.ts
Normal file
@ -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
|
30
web/i18n/lang/custom.zh.ts
Normal file
30
web/i18n/lang/custom.zh.ts
Normal file
@ -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
|
@ -123,6 +123,10 @@ export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
|
||||
providers: Provider[]
|
||||
in_trail: boolean
|
||||
trial_end_reason?: string
|
||||
custom_config?: {
|
||||
remove_webapp_brand?: boolean
|
||||
replace_webapp_logo?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type DataSourceNotionPage = {
|
||||
|
@ -297,7 +297,7 @@ const baseFetch = <T>(
|
||||
]) as Promise<T>
|
||||
}
|
||||
|
||||
export const upload = (options: any, isPublicAPI?: boolean): Promise<any> => {
|
||||
export const upload = (options: any, isPublicAPI?: boolean, url?: string): Promise<any> => {
|
||||
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
|
||||
let token = ''
|
||||
if (isPublicAPI) {
|
||||
@ -318,7 +318,7 @@ export const upload = (options: any, isPublicAPI?: boolean): Promise<any> => {
|
||||
}
|
||||
const defaultOptions = {
|
||||
method: 'POST',
|
||||
url: `${urlPrefix}/files/upload`,
|
||||
url: url ? `${urlPrefix}${url}` : `${urlPrefix}/files/upload`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
|
@ -103,6 +103,10 @@ export const fetchCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; pa
|
||||
return get<ICurrentWorkspace>(url, { params })
|
||||
}
|
||||
|
||||
export const updateCurrentWorkspace: Fetcher<ICurrentWorkspace, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
return post<ICurrentWorkspace>(url, { body })
|
||||
}
|
||||
|
||||
export const fetchWorkspaces: Fetcher<{ workspaces: IWorkspace[] }, { url: string; params: Record<string, any> }> = ({ url, params }) => {
|
||||
return get<{ workspaces: IWorkspace[] }>(url, { params })
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user