Co-authored-by: crazywoola <427733928@qq.com>tags/0.7.1
| parser.add_argument('name', type=str, required=True, location='json') | parser.add_argument('name', type=str, required=True, location='json') | ||||
| parser.add_argument('description', type=str, location='json') | parser.add_argument('description', type=str, location='json') | ||||
| parser.add_argument('mode', type=str, choices=ALLOW_CREATE_APP_MODES, location='json') | parser.add_argument('mode', type=str, choices=ALLOW_CREATE_APP_MODES, location='json') | ||||
| parser.add_argument('icon_type', type=str, location='json') | |||||
| parser.add_argument('icon', type=str, location='json') | parser.add_argument('icon', type=str, location='json') | ||||
| parser.add_argument('icon_background', type=str, location='json') | parser.add_argument('icon_background', type=str, location='json') | ||||
| args = parser.parse_args() | args = parser.parse_args() | ||||
| parser.add_argument('data', type=str, required=True, nullable=False, location='json') | parser.add_argument('data', type=str, required=True, nullable=False, location='json') | ||||
| parser.add_argument('name', type=str, location='json') | parser.add_argument('name', type=str, location='json') | ||||
| parser.add_argument('description', type=str, location='json') | parser.add_argument('description', type=str, location='json') | ||||
| parser.add_argument('icon_type', type=str, location='json') | |||||
| parser.add_argument('icon', type=str, location='json') | parser.add_argument('icon', type=str, location='json') | ||||
| parser.add_argument('icon_background', type=str, location='json') | parser.add_argument('icon_background', type=str, location='json') | ||||
| args = parser.parse_args() | args = parser.parse_args() | ||||
| parser = reqparse.RequestParser() | parser = reqparse.RequestParser() | ||||
| parser.add_argument('name', type=str, required=True, nullable=False, location='json') | parser.add_argument('name', type=str, required=True, nullable=False, location='json') | ||||
| parser.add_argument('description', type=str, location='json') | parser.add_argument('description', type=str, location='json') | ||||
| parser.add_argument('icon_type', type=str, location='json') | |||||
| parser.add_argument('icon', type=str, location='json') | parser.add_argument('icon', type=str, location='json') | ||||
| parser.add_argument('icon_background', type=str, location='json') | parser.add_argument('icon_background', type=str, location='json') | ||||
| parser.add_argument('max_active_requests', type=int, location='json') | parser.add_argument('max_active_requests', type=int, location='json') | ||||
| parser = reqparse.RequestParser() | parser = reqparse.RequestParser() | ||||
| parser.add_argument('name', type=str, location='json') | parser.add_argument('name', type=str, location='json') | ||||
| parser.add_argument('description', type=str, location='json') | parser.add_argument('description', type=str, location='json') | ||||
| parser.add_argument('icon_type', type=str, location='json') | |||||
| parser.add_argument('icon', type=str, location='json') | parser.add_argument('icon', type=str, location='json') | ||||
| parser.add_argument('icon_background', type=str, location='json') | parser.add_argument('icon_background', type=str, location='json') | ||||
| args = parser.parse_args() | args = parser.parse_args() |
| def parse_app_site_args(): | def parse_app_site_args(): | ||||
| parser = reqparse.RequestParser() | parser = reqparse.RequestParser() | ||||
| parser.add_argument('title', type=str, required=False, location='json') | parser.add_argument('title', type=str, required=False, location='json') | ||||
| parser.add_argument('icon_type', type=str, required=False, location='json') | |||||
| parser.add_argument('icon', type=str, required=False, location='json') | parser.add_argument('icon', type=str, required=False, location='json') | ||||
| parser.add_argument('icon_background', type=str, required=False, location='json') | parser.add_argument('icon_background', type=str, required=False, location='json') | ||||
| parser.add_argument('description', type=str, required=False, location='json') | parser.add_argument('description', type=str, required=False, location='json') | ||||
| for attr_name in [ | for attr_name in [ | ||||
| 'title', | 'title', | ||||
| 'icon_type', | |||||
| 'icon', | 'icon', | ||||
| 'icon_background', | 'icon_background', | ||||
| 'description', | 'description', |
| if request.data: | if request.data: | ||||
| parser = reqparse.RequestParser() | parser = reqparse.RequestParser() | ||||
| parser.add_argument('name', type=str, required=False, nullable=True, location='json') | parser.add_argument('name', type=str, required=False, nullable=True, location='json') | ||||
| parser.add_argument('icon_type', type=str, required=False, nullable=True, location='json') | |||||
| parser.add_argument('icon', type=str, required=False, nullable=True, location='json') | parser.add_argument('icon', type=str, required=False, nullable=True, location='json') | ||||
| parser.add_argument('icon_background', type=str, required=False, nullable=True, location='json') | parser.add_argument('icon_background', type=str, required=False, nullable=True, location='json') | ||||
| args = parser.parse_args() | args = parser.parse_args() |
| from controllers.web import api | from controllers.web import api | ||||
| from controllers.web.wraps import WebApiResource | from controllers.web.wraps import WebApiResource | ||||
| from extensions.ext_database import db | from extensions.ext_database import db | ||||
| from libs.helper import AppIconUrlField | |||||
| from models.account import TenantStatus | from models.account import TenantStatus | ||||
| from models.model import Site | from models.model import Site | ||||
| from services.feature_service import FeatureService | from services.feature_service import FeatureService | ||||
| 'title': fields.String, | 'title': fields.String, | ||||
| 'chat_color_theme': fields.String, | 'chat_color_theme': fields.String, | ||||
| 'chat_color_theme_inverted': fields.Boolean, | 'chat_color_theme_inverted': fields.Boolean, | ||||
| 'icon_type': fields.String, | |||||
| 'icon': fields.String, | 'icon': fields.String, | ||||
| 'icon_background': fields.String, | 'icon_background': fields.String, | ||||
| 'icon_url': AppIconUrlField, | |||||
| 'description': fields.String, | 'description': fields.String, | ||||
| 'copyright': fields.String, | 'copyright': fields.String, | ||||
| 'privacy_policy': fields.String, | 'privacy_policy': fields.String, |
| site = Site( | site = Site( | ||||
| app_id=app.id, | app_id=app.id, | ||||
| title=app.name, | title=app.name, | ||||
| icon_type=app.icon_type, | |||||
| icon=app.icon, | icon=app.icon, | ||||
| icon_background=app.icon_background, | icon_background=app.icon_background, | ||||
| default_language=account.interface_language, | default_language=account.interface_language, |
| from flask_restful import fields | from flask_restful import fields | ||||
| from libs.helper import TimestampField | |||||
| from libs.helper import AppIconUrlField, TimestampField | |||||
| app_detail_kernel_fields = { | app_detail_kernel_fields = { | ||||
| "id": fields.String, | "id": fields.String, | ||||
| "name": fields.String, | "name": fields.String, | ||||
| "description": fields.String, | "description": fields.String, | ||||
| "mode": fields.String(attribute="mode_compatible_with_agent"), | "mode": fields.String(attribute="mode_compatible_with_agent"), | ||||
| "icon_type": fields.String, | |||||
| "icon": fields.String, | "icon": fields.String, | ||||
| "icon_background": fields.String, | "icon_background": fields.String, | ||||
| "icon_url": AppIconUrlField, | |||||
| } | } | ||||
| related_app_list = { | related_app_list = { | ||||
| "max_active_requests": fields.Raw(), | "max_active_requests": fields.Raw(), | ||||
| "description": fields.String(attribute="desc_or_prompt"), | "description": fields.String(attribute="desc_or_prompt"), | ||||
| "mode": fields.String(attribute="mode_compatible_with_agent"), | "mode": fields.String(attribute="mode_compatible_with_agent"), | ||||
| "icon_type": fields.String, | |||||
| "icon": fields.String, | "icon": fields.String, | ||||
| "icon_background": fields.String, | "icon_background": fields.String, | ||||
| "icon_url": AppIconUrlField, | |||||
| "model_config": fields.Nested(model_config_partial_fields, attribute="app_model_config", allow_null=True), | "model_config": fields.Nested(model_config_partial_fields, attribute="app_model_config", allow_null=True), | ||||
| "created_at": TimestampField, | "created_at": TimestampField, | ||||
| "tags": fields.List(fields.Nested(tag_fields)), | "tags": fields.List(fields.Nested(tag_fields)), | ||||
| "access_token": fields.String(attribute="code"), | "access_token": fields.String(attribute="code"), | ||||
| "code": fields.String, | "code": fields.String, | ||||
| "title": fields.String, | "title": fields.String, | ||||
| "icon_type": fields.String, | |||||
| "icon": fields.String, | "icon": fields.String, | ||||
| "icon_background": fields.String, | "icon_background": fields.String, | ||||
| "icon_url": AppIconUrlField, | |||||
| "description": fields.String, | "description": fields.String, | ||||
| "default_language": fields.String, | "default_language": fields.String, | ||||
| "chat_color_theme": fields.String, | "chat_color_theme": fields.String, | ||||
| "name": fields.String, | "name": fields.String, | ||||
| "description": fields.String, | "description": fields.String, | ||||
| "mode": fields.String(attribute="mode_compatible_with_agent"), | "mode": fields.String(attribute="mode_compatible_with_agent"), | ||||
| "icon_type": fields.String, | |||||
| "icon": fields.String, | "icon": fields.String, | ||||
| "icon_background": fields.String, | "icon_background": fields.String, | ||||
| "icon_url": AppIconUrlField, | |||||
| "enable_site": fields.Boolean, | "enable_site": fields.Boolean, | ||||
| "enable_api": fields.Boolean, | "enable_api": fields.Boolean, | ||||
| "model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True), | "model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True), |
| from flask_restful import fields | from flask_restful import fields | ||||
| from libs.helper import TimestampField | |||||
| from libs.helper import AppIconUrlField, TimestampField | |||||
| app_fields = { | app_fields = { | ||||
| "id": fields.String, | "id": fields.String, | ||||
| "name": fields.String, | "name": fields.String, | ||||
| "mode": fields.String, | "mode": fields.String, | ||||
| "icon_type": fields.String, | |||||
| "icon": fields.String, | "icon": fields.String, | ||||
| "icon_background": fields.String, | "icon_background": fields.String, | ||||
| "icon_url": AppIconUrlField, | |||||
| } | } | ||||
| installed_app_fields = { | installed_app_fields = { |
| from flask_restful import fields | from flask_restful import fields | ||||
| from core.app.features.rate_limiting.rate_limit import RateLimitGenerator | from core.app.features.rate_limiting.rate_limit import RateLimitGenerator | ||||
| from core.file.upload_file_parser import UploadFileParser | |||||
| from extensions.ext_redis import redis_client | from extensions.ext_redis import redis_client | ||||
| from models.account import Account | from models.account import Account | ||||
| return subprocess.getstatusoutput("source /root/.bashrc && " + script) | return subprocess.getstatusoutput("source /root/.bashrc && " + script) | ||||
| class AppIconUrlField(fields.Raw): | |||||
| def output(self, key, obj): | |||||
| if obj is None: | |||||
| return None | |||||
| from models.model import IconType | |||||
| if obj.icon_type == IconType.IMAGE.value: | |||||
| return UploadFileParser.get_signed_temp_image_url(obj.icon) | |||||
| return None | |||||
| class TimestampField(fields.Raw): | class TimestampField(fields.Raw): | ||||
| def format(self, value) -> int: | def format(self, value) -> int: | ||||
| return int(value.timestamp()) | return int(value.timestamp()) |
| """app and site icon type | |||||
| Revision ID: a6be81136580 | |||||
| Revises: 8782057ff0dc | |||||
| Create Date: 2024-08-15 10:01:24.697888 | |||||
| """ | |||||
| import sqlalchemy as sa | |||||
| from alembic import op | |||||
| import models as models | |||||
| # revision identifiers, used by Alembic. | |||||
| revision = 'a6be81136580' | |||||
| down_revision = '8782057ff0dc' | |||||
| branch_labels = None | |||||
| depends_on = None | |||||
| def upgrade(): | |||||
| # ### commands auto generated by Alembic - please adjust! ### | |||||
| with op.batch_alter_table('apps', schema=None) as batch_op: | |||||
| batch_op.add_column(sa.Column('icon_type', sa.String(length=255), nullable=True)) | |||||
| with op.batch_alter_table('sites', schema=None) as batch_op: | |||||
| batch_op.add_column(sa.Column('icon_type', sa.String(length=255), nullable=True)) | |||||
| # ### end Alembic commands ### | |||||
| def downgrade(): | |||||
| # ### commands auto generated by Alembic - please adjust! ### | |||||
| with op.batch_alter_table('sites', schema=None) as batch_op: | |||||
| batch_op.drop_column('icon_type') | |||||
| with op.batch_alter_table('apps', schema=None) as batch_op: | |||||
| batch_op.drop_column('icon_type') | |||||
| # ### end Alembic commands ### |
| raise ValueError(f'invalid mode value {value}') | raise ValueError(f'invalid mode value {value}') | ||||
| class IconType(Enum): | |||||
| IMAGE = "image" | |||||
| EMOJI = "emoji" | |||||
| class App(db.Model): | class App(db.Model): | ||||
| __tablename__ = 'apps' | __tablename__ = 'apps' | ||||
| __table_args__ = ( | __table_args__ = ( | ||||
| name = db.Column(db.String(255), nullable=False) | name = db.Column(db.String(255), nullable=False) | ||||
| description = db.Column(db.Text, nullable=False, server_default=db.text("''::character varying")) | description = db.Column(db.Text, nullable=False, server_default=db.text("''::character varying")) | ||||
| mode = db.Column(db.String(255), nullable=False) | mode = db.Column(db.String(255), nullable=False) | ||||
| icon_type = db.Column(db.String(255), nullable=True) | |||||
| icon = db.Column(db.String(255)) | icon = db.Column(db.String(255)) | ||||
| icon_background = db.Column(db.String(255)) | icon_background = db.Column(db.String(255)) | ||||
| app_model_config_id = db.Column(StringUUID, nullable=True) | app_model_config_id = db.Column(StringUUID, nullable=True) | ||||
| id = db.Column(StringUUID, server_default=db.text('uuid_generate_v4()')) | id = db.Column(StringUUID, server_default=db.text('uuid_generate_v4()')) | ||||
| app_id = db.Column(StringUUID, nullable=False) | app_id = db.Column(StringUUID, nullable=False) | ||||
| title = db.Column(db.String(255), nullable=False) | title = db.Column(db.String(255), nullable=False) | ||||
| icon_type = db.Column(db.String(255), nullable=True) | |||||
| icon = db.Column(db.String(255)) | icon = db.Column(db.String(255)) | ||||
| icon_background = db.Column(db.String(255)) | icon_background = db.Column(db.String(255)) | ||||
| description = db.Column(db.Text) | description = db.Column(db.Text) |
| # get app basic info | # get app basic info | ||||
| name = args.get("name") if args.get("name") else app_data.get('name') | name = args.get("name") if args.get("name") else app_data.get('name') | ||||
| description = args.get("description") if args.get("description") else app_data.get('description', '') | description = args.get("description") if args.get("description") else app_data.get('description', '') | ||||
| icon_type = args.get("icon_type") if args.get("icon_type") else app_data.get('icon_type') | |||||
| icon = args.get("icon") if args.get("icon") else app_data.get('icon') | icon = args.get("icon") if args.get("icon") else app_data.get('icon') | ||||
| icon_background = args.get("icon_background") if args.get("icon_background") \ | icon_background = args.get("icon_background") if args.get("icon_background") \ | ||||
| else app_data.get('icon_background') | else app_data.get('icon_background') | ||||
| account=account, | account=account, | ||||
| name=name, | name=name, | ||||
| description=description, | description=description, | ||||
| icon_type=icon_type, | |||||
| icon=icon, | icon=icon, | ||||
| icon_background=icon_background | icon_background=icon_background | ||||
| ) | ) | ||||
| account=account, | account=account, | ||||
| name=name, | name=name, | ||||
| description=description, | description=description, | ||||
| icon_type=icon_type, | |||||
| icon=icon, | icon=icon, | ||||
| icon_background=icon_background | icon_background=icon_background | ||||
| ) | ) | ||||
| "app": { | "app": { | ||||
| "name": app_model.name, | "name": app_model.name, | ||||
| "mode": app_model.mode, | "mode": app_model.mode, | ||||
| "icon": app_model.icon, | |||||
| "icon_background": app_model.icon_background, | |||||
| "icon": '🤖' if app_model.icon_type == 'image' else app_model.icon, | |||||
| "icon_background": '#FFEAD5' if app_model.icon_type == 'image' else app_model.icon_background, | |||||
| "description": app_model.description | "description": app_model.description | ||||
| } | } | ||||
| } | } | ||||
| account: Account, | account: Account, | ||||
| name: str, | name: str, | ||||
| description: str, | description: str, | ||||
| icon_type: str, | |||||
| icon: str, | icon: str, | ||||
| icon_background: str) -> App: | icon_background: str) -> App: | ||||
| """ | """ | ||||
| :param account: Account instance | :param account: Account instance | ||||
| :param name: app name | :param name: app name | ||||
| :param description: app description | :param description: app description | ||||
| :param icon_type: app icon type, "emoji" or "image" | |||||
| :param icon: app icon | :param icon: app icon | ||||
| :param icon_background: app icon background | :param icon_background: app icon background | ||||
| """ | """ | ||||
| account=account, | account=account, | ||||
| name=name, | name=name, | ||||
| description=description, | description=description, | ||||
| icon_type=icon_type, | |||||
| icon=icon, | icon=icon, | ||||
| icon_background=icon_background | icon_background=icon_background | ||||
| ) | ) | ||||
| account: Account, | account: Account, | ||||
| name: str, | name: str, | ||||
| description: str, | description: str, | ||||
| icon_type: str, | |||||
| icon: str, | icon: str, | ||||
| icon_background: str) -> App: | icon_background: str) -> App: | ||||
| """ | """ | ||||
| account=account, | account=account, | ||||
| name=name, | name=name, | ||||
| description=description, | description=description, | ||||
| icon_type=icon_type, | |||||
| icon=icon, | icon=icon, | ||||
| icon_background=icon_background | icon_background=icon_background | ||||
| ) | ) | ||||
| account: Account, | account: Account, | ||||
| name: str, | name: str, | ||||
| description: str, | description: str, | ||||
| icon_type: str, | |||||
| icon: str, | icon: str, | ||||
| icon_background: str) -> App: | icon_background: str) -> App: | ||||
| """ | """ | ||||
| :param account: Account instance | :param account: Account instance | ||||
| :param name: app name | :param name: app name | ||||
| :param description: app description | :param description: app description | ||||
| :param icon_type: app icon type, "emoji" or "image" | |||||
| :param icon: app icon | :param icon: app icon | ||||
| :param icon_background: app icon background | :param icon_background: app icon background | ||||
| """ | """ | ||||
| mode=app_mode.value, | mode=app_mode.value, | ||||
| name=name, | name=name, | ||||
| description=description, | description=description, | ||||
| icon_type=icon_type, | |||||
| icon=icon, | icon=icon, | ||||
| icon_background=icon_background, | icon_background=icon_background, | ||||
| enable_site=True, | enable_site=True, |
| app.name = args['name'] | app.name = args['name'] | ||||
| app.description = args.get('description', '') | app.description = args.get('description', '') | ||||
| app.mode = args['mode'] | app.mode = args['mode'] | ||||
| app.icon_type = args.get('icon_type', 'emoji') | |||||
| app.icon = args['icon'] | app.icon = args['icon'] | ||||
| app.icon_background = args['icon_background'] | app.icon_background = args['icon_background'] | ||||
| app.tenant_id = tenant_id | app.tenant_id = tenant_id | ||||
| app.name = args.get('name') | app.name = args.get('name') | ||||
| app.description = args.get('description', '') | app.description = args.get('description', '') | ||||
| app.max_active_requests = args.get('max_active_requests') | app.max_active_requests = args.get('max_active_requests') | ||||
| app.icon_type = args.get('icon_type', 'emoji') | |||||
| app.icon = args.get('icon') | app.icon = args.get('icon') | ||||
| app.icon_background = args.get('icon_background') | app.icon_background = args.get('icon_background') | ||||
| app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) | app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) |
| def convert_to_workflow(self, app_model: App, | def convert_to_workflow(self, app_model: App, | ||||
| account: Account, | account: Account, | ||||
| name: str, | name: str, | ||||
| icon_type: str, | |||||
| icon: str, | icon: str, | ||||
| icon_background: str) -> App: | icon_background: str) -> App: | ||||
| """ | """ | ||||
| :param account: Account | :param account: Account | ||||
| :param name: new app name | :param name: new app name | ||||
| :param icon: new app icon | :param icon: new app icon | ||||
| :param icon_type: new app icon type | |||||
| :param icon_background: new app icon background | :param icon_background: new app icon background | ||||
| :return: new App instance | :return: new App instance | ||||
| """ | """ | ||||
| new_app.name = name if name else app_model.name + '(workflow)' | new_app.name = name if name else app_model.name + '(workflow)' | ||||
| new_app.mode = AppMode.ADVANCED_CHAT.value \ | new_app.mode = AppMode.ADVANCED_CHAT.value \ | ||||
| if app_model.mode == AppMode.CHAT.value else AppMode.WORKFLOW.value | if app_model.mode == AppMode.CHAT.value else AppMode.WORKFLOW.value | ||||
| new_app.icon_type = icon_type if icon_type else app_model.icon_type | |||||
| new_app.icon = icon if icon else app_model.icon | new_app.icon = icon if icon else app_model.icon | ||||
| new_app.icon_background = icon_background if icon_background else app_model.icon_background | new_app.icon_background = icon_background if icon_background else app_model.icon_background | ||||
| new_app.enable_site = app_model.enable_site | new_app.enable_site = app_model.enable_site |
| app_model=app_model, | app_model=app_model, | ||||
| account=account, | account=account, | ||||
| name=args.get('name'), | name=args.get('name'), | ||||
| icon_type=args.get('icon_type'), | |||||
| icon=args.get('icon'), | icon=args.get('icon'), | ||||
| icon_background=args.get('icon_background'), | icon_background=args.get('icon_background'), | ||||
| ) | ) |
| NEXT_PUBLIC_SENTRY_DSN= | NEXT_PUBLIC_SENTRY_DSN= | ||||
| # Disable Next.js Telemetry (https://nextjs.org/telemetry) | # Disable Next.js Telemetry (https://nextjs.org/telemetry) | ||||
| NEXT_TELEMETRY_DISABLED=1 | |||||
| NEXT_TELEMETRY_DISABLED=1 | |||||
| # Disable Upload Image as WebApp icon default is false | |||||
| NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false |
| const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ | const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ | ||||
| name, | name, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| description, | description, | ||||
| await updateAppInfo({ | await updateAppInfo({ | ||||
| appID: app.id, | appID: app.id, | ||||
| name, | name, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| description, | description, | ||||
| } | } | ||||
| }, [app.id, mutateApps, notify, onRefresh, t]) | }, [app.id, mutateApps, notify, onRefresh, t]) | ||||
| const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => { | |||||
| const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { | |||||
| try { | try { | ||||
| const newApp = await copyApp({ | const newApp = await copyApp({ | ||||
| appID: app.id, | appID: app.id, | ||||
| name, | name, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| mode: app.mode, | mode: app.mode, | ||||
| <div className='relative shrink-0'> | <div className='relative shrink-0'> | ||||
| <AppIcon | <AppIcon | ||||
| size="large" | size="large" | ||||
| iconType={app.icon_type} | |||||
| icon={app.icon} | icon={app.icon} | ||||
| background={app.icon_background} | background={app.icon_background} | ||||
| imageUrl={app.icon_url} | |||||
| /> | /> | ||||
| <span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'> | <span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'> | ||||
| {app.mode === 'advanced-chat' && ( | {app.mode === 'advanced-chat' && ( | ||||
| {showEditModal && ( | {showEditModal && ( | ||||
| <EditAppModal | <EditAppModal | ||||
| isEditModal | isEditModal | ||||
| appName={app.name} | |||||
| appIconType={app.icon_type} | |||||
| appIcon={app.icon} | appIcon={app.icon} | ||||
| appIconBackground={app.icon_background} | appIconBackground={app.icon_background} | ||||
| appName={app.name} | |||||
| appIconUrl={app.icon_url} | |||||
| appDescription={app.description} | appDescription={app.description} | ||||
| show={showEditModal} | show={showEditModal} | ||||
| onConfirm={onEdit} | onConfirm={onEdit} | ||||
| {showDuplicateModal && ( | {showDuplicateModal && ( | ||||
| <DuplicateAppModal | <DuplicateAppModal | ||||
| appName={app.name} | appName={app.name} | ||||
| icon_type={app.icon_type} | |||||
| icon={app.icon} | icon={app.icon} | ||||
| icon_background={app.icon_background} | icon_background={app.icon_background} | ||||
| icon_url={app.icon_url} | |||||
| show={showDuplicateModal} | show={showDuplicateModal} | ||||
| onConfirm={onCopy} | onConfirm={onCopy} | ||||
| onHide={() => setShowDuplicateModal(false)} | onHide={() => setShowDuplicateModal(false)} |
| return ( | return ( | ||||
| <Link className={classNames(s.itemWrapper, 'px-2', isMobile && 'justify-center')} href={`/app/${detail?.id}/overview`}> | <Link className={classNames(s.itemWrapper, 'px-2', isMobile && 'justify-center')} href={`/app/${detail?.id}/overview`}> | ||||
| <div className={classNames(s.iconWrapper, 'mr-0')}> | <div className={classNames(s.iconWrapper, 'mr-0')}> | ||||
| <AppIcon size='tiny' icon={detail?.icon} background={detail?.icon_background} /> | |||||
| <AppIcon size='tiny' iconType={detail.icon_type} icon={detail.icon} background={detail.icon_background} imageUrl={detail.icon_url} /> | |||||
| {type === 'app' && ( | {type === 'app' && ( | ||||
| <span className='absolute bottom-[-2px] right-[-2px] w-3.5 h-3.5 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'> | <span className='absolute bottom-[-2px] right-[-2px] w-3.5 h-3.5 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'> | ||||
| {detail.mode === 'advanced-chat' && ( | {detail.mode === 'advanced-chat' && ( |
| const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ | const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ | ||||
| name, | name, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| description, | description, | ||||
| const app = await updateAppInfo({ | const app = await updateAppInfo({ | ||||
| appID: appDetail.id, | appID: appDetail.id, | ||||
| name, | name, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| description, | description, | ||||
| } | } | ||||
| }, [appDetail, mutateApps, notify, setAppDetail, t]) | }, [appDetail, mutateApps, notify, setAppDetail, t]) | ||||
| const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => { | |||||
| const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { | |||||
| if (!appDetail) | if (!appDetail) | ||||
| return | return | ||||
| try { | try { | ||||
| const newApp = await copyApp({ | const newApp = await copyApp({ | ||||
| appID: appDetail.id, | appID: appDetail.id, | ||||
| name, | name, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| mode: appDetail.mode, | mode: appDetail.mode, | ||||
| > | > | ||||
| <div className={cn('flex p-1 rounded-lg', open && 'bg-gray-100', isCurrentWorkspaceEditor && 'hover:bg-gray-100 cursor-pointer')}> | <div className={cn('flex p-1 rounded-lg', open && 'bg-gray-100', isCurrentWorkspaceEditor && 'hover:bg-gray-100 cursor-pointer')}> | ||||
| <div className='relative shrink-0 mr-2'> | <div className='relative shrink-0 mr-2'> | ||||
| <AppIcon size={expand ? 'large' : 'small'} icon={appDetail.icon} background={appDetail.icon_background} /> | |||||
| <AppIcon | |||||
| size={expand ? 'large' : 'small'} | |||||
| iconType={appDetail.icon_type} | |||||
| icon={appDetail.icon} | |||||
| background={appDetail.icon_background} | |||||
| imageUrl={appDetail.icon_url} | |||||
| /> | |||||
| <span className={cn( | <span className={cn( | ||||
| 'absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm', | 'absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm', | ||||
| !expand && '!w-3.5 !h-3.5 !bottom-[-2px] !right-[-2px]', | !expand && '!w-3.5 !h-3.5 !bottom-[-2px] !right-[-2px]', | ||||
| {/* header */} | {/* header */} | ||||
| <div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-2')}> | <div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-2')}> | ||||
| <div className='relative shrink-0 mr-2'> | <div className='relative shrink-0 mr-2'> | ||||
| <AppIcon size="large" icon={appDetail.icon} background={appDetail.icon_background} /> | |||||
| <AppIcon | |||||
| size="large" | |||||
| iconType={appDetail.icon_type} | |||||
| icon={appDetail.icon} | |||||
| background={appDetail.icon_background} | |||||
| imageUrl={appDetail.icon_url} | |||||
| /> | |||||
| <span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'> | <span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'> | ||||
| {appDetail.mode === 'advanced-chat' && ( | {appDetail.mode === 'advanced-chat' && ( | ||||
| <ChatBot className='w-3 h-3 text-[#1570EF]' /> | <ChatBot className='w-3 h-3 text-[#1570EF]' /> | ||||
| {showEditModal && ( | {showEditModal && ( | ||||
| <CreateAppModal | <CreateAppModal | ||||
| isEditModal | isEditModal | ||||
| appName={appDetail.name} | |||||
| appIconType={appDetail.icon_type} | |||||
| appIcon={appDetail.icon} | appIcon={appDetail.icon} | ||||
| appIconBackground={appDetail.icon_background} | appIconBackground={appDetail.icon_background} | ||||
| appName={appDetail.name} | |||||
| appIconUrl={appDetail.icon_url} | |||||
| appDescription={appDetail.description} | appDescription={appDetail.description} | ||||
| show={showEditModal} | show={showEditModal} | ||||
| onConfirm={onEdit} | onConfirm={onEdit} | ||||
| {showDuplicateModal && ( | {showDuplicateModal && ( | ||||
| <DuplicateAppModal | <DuplicateAppModal | ||||
| appName={appDetail.name} | appName={appDetail.name} | ||||
| icon_type={appDetail.icon_type} | |||||
| icon={appDetail.icon} | icon={appDetail.icon} | ||||
| icon_background={appDetail.icon_background} | icon_background={appDetail.icon_background} | ||||
| icon_url={appDetail.icon_url} | |||||
| show={showDuplicateModal} | show={showDuplicateModal} | ||||
| onConfirm={onCopy} | onConfirm={onCopy} | ||||
| onHide={() => setShowDuplicateModal(false)} | onHide={() => setShowDuplicateModal(false)} |
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import { useRouter } from 'next/navigation' | import { useRouter } from 'next/navigation' | ||||
| import { useContext, useContextSelector } from 'use-context-selector' | import { useContext, useContextSelector } from 'use-context-selector' | ||||
| import AppIconPicker from '../../base/app-icon-picker' | |||||
| import type { AppIconSelection } from '../../base/app-icon-picker' | |||||
| import s from './style.module.css' | import s from './style.module.css' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import AppsContext, { useAppContext } from '@/context/app-context' | import AppsContext, { useAppContext } from '@/context/app-context' | ||||
| import Modal from '@/app/components/base/modal' | import Modal from '@/app/components/base/modal' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import EmojiPicker from '@/app/components/base/emoji-picker' | |||||
| import AppsFull from '@/app/components/billing/apps-full-in-dialog' | import AppsFull from '@/app/components/billing/apps-full-in-dialog' | ||||
| import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication' | import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication' | ||||
| import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel' | import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel' | ||||
| const [appMode, setAppMode] = useState<AppMode>('chat') | const [appMode, setAppMode] = useState<AppMode>('chat') | ||||
| const [showChatBotType, setShowChatBotType] = useState<boolean>(true) | const [showChatBotType, setShowChatBotType] = useState<boolean>(true) | ||||
| const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' }) | |||||
| const [showEmojiPicker, setShowEmojiPicker] = useState(false) | |||||
| const [appIcon, setAppIcon] = useState<AppIconSelection>({ type: 'emoji', icon: '🤖', background: '#FFEAD5' }) | |||||
| const [showAppIconPicker, setShowAppIconPicker] = useState(false) | |||||
| const [name, setName] = useState('') | const [name, setName] = useState('') | ||||
| const [description, setDescription] = useState('') | const [description, setDescription] = useState('') | ||||
| const app = await createApp({ | const app = await createApp({ | ||||
| name, | name, | ||||
| description, | description, | ||||
| icon: emoji.icon, | |||||
| icon_background: emoji.icon_background, | |||||
| icon_type: appIcon.type, | |||||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||||
| icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, | |||||
| mode: appMode, | mode: appMode, | ||||
| }) | }) | ||||
| notify({ type: 'success', message: t('app.newApp.appCreated') }) | notify({ type: 'success', message: t('app.newApp.appCreated') }) | ||||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | ||||
| } | } | ||||
| isCreatingRef.current = false | isCreatingRef.current = false | ||||
| }, [name, notify, t, appMode, emoji.icon, emoji.icon_background, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) | |||||
| }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) | |||||
| return ( | return ( | ||||
| <Modal | <Modal | ||||
| <div className='pt-2 px-8'> | <div className='pt-2 px-8'> | ||||
| <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionName')}</div> | <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionName')}</div> | ||||
| <div className='flex items-center justify-between space-x-2'> | <div className='flex items-center justify-between space-x-2'> | ||||
| <AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> | |||||
| <AppIcon | |||||
| iconType={appIcon.type} | |||||
| icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId} | |||||
| background={appIcon.type === 'emoji' ? appIcon.background : undefined} | |||||
| imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} | |||||
| size='large' className='cursor-pointer' | |||||
| onClick={() => { setShowAppIconPicker(true) }} | |||||
| /> | |||||
| <input | <input | ||||
| value={name} | value={name} | ||||
| onChange={e => setName(e.target.value)} | onChange={e => setName(e.target.value)} | ||||
| className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs' | className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs' | ||||
| /> | /> | ||||
| </div> | </div> | ||||
| {showEmojiPicker && <EmojiPicker | |||||
| onSelect={(icon, icon_background) => { | |||||
| setEmoji({ icon, icon_background }) | |||||
| setShowEmojiPicker(false) | |||||
| {showAppIconPicker && <AppIconPicker | |||||
| onSelect={(payload) => { | |||||
| setAppIcon(payload) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| onClose={() => { | onClose={() => { | ||||
| setEmoji({ icon: '🤖', icon_background: '#FFEAD5' }) | |||||
| setShowEmojiPicker(false) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| />} | />} | ||||
| </div> | </div> |
| 'use client' | 'use client' | ||||
| import React, { useState } from 'react' | import React, { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import AppIconPicker from '../../base/app-icon-picker' | |||||
| import s from './style.module.css' | import s from './style.module.css' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import Modal from '@/app/components/base/modal' | import Modal from '@/app/components/base/modal' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import EmojiPicker from '@/app/components/base/emoji-picker' | |||||
| import { useProviderContext } from '@/context/provider-context' | import { useProviderContext } from '@/context/provider-context' | ||||
| import AppsFull from '@/app/components/billing/apps-full-in-dialog' | import AppsFull from '@/app/components/billing/apps-full-in-dialog' | ||||
| import type { AppIconType } from '@/types/app' | |||||
| export type DuplicateAppModalProps = { | export type DuplicateAppModalProps = { | ||||
| appName: string | appName: string | ||||
| icon_type: AppIconType | null | |||||
| icon: string | icon: string | ||||
| icon_background: string | |||||
| icon_background?: string | null | |||||
| icon_url?: string | null | |||||
| show: boolean | show: boolean | ||||
| onConfirm: (info: { | onConfirm: (info: { | ||||
| name: string | name: string | ||||
| icon_type: AppIconType | |||||
| icon: string | icon: string | ||||
| icon_background: string | |||||
| icon_background?: string | null | |||||
| }) => Promise<void> | }) => Promise<void> | ||||
| onHide: () => void | onHide: () => void | ||||
| } | } | ||||
| const DuplicateAppModal = ({ | const DuplicateAppModal = ({ | ||||
| appName, | appName, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| icon_url, | |||||
| show = false, | show = false, | ||||
| onConfirm, | onConfirm, | ||||
| onHide, | onHide, | ||||
| const [name, setName] = React.useState(appName) | const [name, setName] = React.useState(appName) | ||||
| const [showEmojiPicker, setShowEmojiPicker] = useState(false) | |||||
| const [emoji, setEmoji] = useState({ icon, icon_background }) | |||||
| const [showAppIconPicker, setShowAppIconPicker] = useState(false) | |||||
| const [appIcon, setAppIcon] = useState( | |||||
| icon_type === 'image' | |||||
| ? { type: 'image' as const, url: icon_url, fileId: icon } | |||||
| : { type: 'emoji' as const, icon, background: icon_background }, | |||||
| ) | |||||
| const { plan, enableBilling } = useProviderContext() | const { plan, enableBilling } = useProviderContext() | ||||
| const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | ||||
| } | } | ||||
| onConfirm({ | onConfirm({ | ||||
| name, | name, | ||||
| ...emoji, | |||||
| icon_type: appIcon.type, | |||||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||||
| icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, | |||||
| }) | }) | ||||
| onHide() | onHide() | ||||
| } | } | ||||
| <div className={s.content}> | <div className={s.content}> | ||||
| <div className={s.subTitle}>{t('explore.appCustomize.subTitle')}</div> | <div className={s.subTitle}>{t('explore.appCustomize.subTitle')}</div> | ||||
| <div className='flex items-center justify-between space-x-2'> | <div className='flex items-center justify-between space-x-2'> | ||||
| <AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> | |||||
| <AppIcon | |||||
| size='large' | |||||
| onClick={() => { setShowAppIconPicker(true) }} | |||||
| className='cursor-pointer' | |||||
| iconType={appIcon.type} | |||||
| icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon} | |||||
| background={appIcon.type === 'image' ? undefined : appIcon.background} | |||||
| imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} | |||||
| /> | |||||
| <input | <input | ||||
| value={name} | value={name} | ||||
| onChange={e => setName(e.target.value)} | onChange={e => setName(e.target.value)} | ||||
| <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button> | <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button> | ||||
| </div> | </div> | ||||
| </Modal> | </Modal> | ||||
| {showEmojiPicker && <EmojiPicker | |||||
| onSelect={(icon, icon_background) => { | |||||
| setEmoji({ icon, icon_background }) | |||||
| setShowEmojiPicker(false) | |||||
| {showAppIconPicker && <AppIconPicker | |||||
| onSelect={(payload) => { | |||||
| setAppIcon(payload) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| onClose={() => { | onClose={() => { | ||||
| setEmoji({ icon, icon_background }) | |||||
| setShowEmojiPicker(false) | |||||
| setAppIcon(icon_type === 'image' | |||||
| ? { type: 'image', url: icon_url!, fileId: icon } | |||||
| : { type: 'emoji', icon, background: icon_background! }) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| />} | />} | ||||
| </> | </> |
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import { SimpleSelect } from '@/app/components/base/select' | import { SimpleSelect } from '@/app/components/base/select' | ||||
| import type { AppDetailResponse } from '@/models/app' | import type { AppDetailResponse } from '@/models/app' | ||||
| import type { Language } from '@/types/app' | |||||
| import EmojiPicker from '@/app/components/base/emoji-picker' | |||||
| import type { AppIconType, Language } from '@/types/app' | |||||
| import { useToastContext } from '@/app/components/base/toast' | import { useToastContext } from '@/app/components/base/toast' | ||||
| import { languages } from '@/i18n/language' | import { languages } from '@/i18n/language' | ||||
| import type { AppIconSelection } from '@/app/components/base/app-icon-picker' | |||||
| import AppIconPicker from '@/app/components/base/app-icon-picker' | |||||
| export type ISettingsModalProps = { | export type ISettingsModalProps = { | ||||
| isChat: boolean | isChat: boolean | ||||
| copyright: string | copyright: string | ||||
| privacy_policy: string | privacy_policy: string | ||||
| custom_disclaimer: string | custom_disclaimer: string | ||||
| icon_type: AppIconType | |||||
| icon: string | icon: string | ||||
| icon_background: string | |||||
| icon_background?: string | |||||
| show_workflow_steps: boolean | show_workflow_steps: boolean | ||||
| } | } | ||||
| }) => { | }) => { | ||||
| const { notify } = useToastContext() | const { notify } = useToastContext() | ||||
| const [isShowMore, setIsShowMore] = useState(false) | const [isShowMore, setIsShowMore] = useState(false) | ||||
| const { icon, icon_background } = appInfo | |||||
| const { | const { | ||||
| title, | title, | ||||
| icon_type, | |||||
| icon, | |||||
| icon_background, | |||||
| icon_url, | |||||
| description, | description, | ||||
| chat_color_theme, | chat_color_theme, | ||||
| chat_color_theme_inverted, | chat_color_theme_inverted, | ||||
| const [language, setLanguage] = useState(default_language) | const [language, setLanguage] = useState(default_language) | ||||
| const [saveLoading, setSaveLoading] = useState(false) | const [saveLoading, setSaveLoading] = useState(false) | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| // Emoji Picker | |||||
| const [showEmojiPicker, setShowEmojiPicker] = useState(false) | |||||
| const [emoji, setEmoji] = useState({ icon, icon_background }) | |||||
| const [showAppIconPicker, setShowAppIconPicker] = useState(false) | |||||
| const [appIcon, setAppIcon] = useState<AppIconSelection>( | |||||
| icon_type === 'image' | |||||
| ? { type: 'image', url: icon_url!, fileId: icon } | |||||
| : { type: 'emoji', icon, background: icon_background! }, | |||||
| ) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setInputInfo({ | setInputInfo({ | ||||
| show_workflow_steps, | show_workflow_steps, | ||||
| }) | }) | ||||
| setLanguage(default_language) | setLanguage(default_language) | ||||
| setEmoji({ icon, icon_background }) | |||||
| setAppIcon(icon_type === 'image' | |||||
| ? { type: 'image', url: icon_url!, fileId: icon } | |||||
| : { type: 'emoji', icon, background: icon_background! }) | |||||
| }, [appInfo]) | }, [appInfo]) | ||||
| const onHide = () => { | const onHide = () => { | ||||
| copyright: inputInfo.copyright, | copyright: inputInfo.copyright, | ||||
| privacy_policy: inputInfo.privacyPolicy, | privacy_policy: inputInfo.privacyPolicy, | ||||
| custom_disclaimer: inputInfo.customDisclaimer, | custom_disclaimer: inputInfo.customDisclaimer, | ||||
| icon: emoji.icon, | |||||
| icon_background: emoji.icon_background, | |||||
| icon_type: appIcon.type, | |||||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||||
| icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, | |||||
| show_workflow_steps: inputInfo.show_workflow_steps, | show_workflow_steps: inputInfo.show_workflow_steps, | ||||
| } | } | ||||
| await onSave?.(params) | await onSave?.(params) | ||||
| <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div> | <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div> | ||||
| <div className='flex mt-2'> | <div className='flex mt-2'> | ||||
| <AppIcon size='large' | <AppIcon size='large' | ||||
| onClick={() => { setShowEmojiPicker(true) }} | |||||
| onClick={() => { setShowAppIconPicker(true) }} | |||||
| className='cursor-pointer !mr-3 self-center' | className='cursor-pointer !mr-3 self-center' | ||||
| icon={emoji.icon} | |||||
| background={emoji.icon_background} | |||||
| iconType={appIcon.type} | |||||
| icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon} | |||||
| background={appIcon.type === 'image' ? undefined : appIcon.background} | |||||
| imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} | |||||
| /> | /> | ||||
| <input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`} | <input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`} | ||||
| value={inputInfo.title} | value={inputInfo.title} | ||||
| <Button className='mr-2' onClick={onHide}>{t('common.operation.cancel')}</Button> | <Button className='mr-2' onClick={onHide}>{t('common.operation.cancel')}</Button> | ||||
| <Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button> | <Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button> | ||||
| </div> | </div> | ||||
| {showEmojiPicker && <EmojiPicker | |||||
| onSelect={(icon, icon_background) => { | |||||
| setEmoji({ icon, icon_background }) | |||||
| setShowEmojiPicker(false) | |||||
| {showAppIconPicker && <AppIconPicker | |||||
| onSelect={(payload) => { | |||||
| setAppIcon(payload) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| onClose={() => { | onClose={() => { | ||||
| setEmoji({ icon: appInfo.site.icon, icon_background: appInfo.site.icon_background }) | |||||
| setShowEmojiPicker(false) | |||||
| setAppIcon(icon_type === 'image' | |||||
| ? { type: 'image', url: icon_url!, fileId: icon } | |||||
| : { type: 'emoji', icon, background: icon_background! }) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| />} | />} | ||||
| </Modal > | </Modal > |
| import { useContext } from 'use-context-selector' | import { useContext } from 'use-context-selector' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { RiCloseLine } from '@remixicon/react' | import { RiCloseLine } from '@remixicon/react' | ||||
| import AppIconPicker from '../../base/app-icon-picker' | |||||
| import s from './style.module.css' | import s from './style.module.css' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { useAppContext } from '@/context/app-context' | import { useAppContext } from '@/context/app-context' | ||||
| import { useProviderContext } from '@/context/provider-context' | import { useProviderContext } from '@/context/provider-context' | ||||
| import AppsFull from '@/app/components/billing/apps-full-in-dialog' | import AppsFull from '@/app/components/billing/apps-full-in-dialog' | ||||
| import EmojiPicker from '@/app/components/base/emoji-picker' | |||||
| import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | ||||
| import { getRedirection } from '@/utils/app-redirection' | import { getRedirection } from '@/utils/app-redirection' | ||||
| import type { App } from '@/types/app' | import type { App } from '@/types/app' | ||||
| const { plan, enableBilling } = useProviderContext() | const { plan, enableBilling } = useProviderContext() | ||||
| const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | ||||
| const [emoji, setEmoji] = useState({ icon: appDetail.icon, icon_background: appDetail.icon_background }) | |||||
| const [showEmojiPicker, setShowEmojiPicker] = useState(false) | |||||
| const [showAppIconPicker, setShowAppIconPicker] = useState(false) | |||||
| const [appIcon, setAppIcon] = useState( | |||||
| appDetail.icon_type === 'image' | |||||
| ? { type: 'image' as const, url: appDetail.icon_url, fileId: appDetail.icon } | |||||
| : { type: 'emoji' as const, icon: appDetail.icon, background: appDetail.icon_background }, | |||||
| ) | |||||
| const [name, setName] = useState(`${appDetail.name}(copy)`) | const [name, setName] = useState(`${appDetail.name}(copy)`) | ||||
| const [removeOriginal, setRemoveOriginal] = useState<boolean>(false) | const [removeOriginal, setRemoveOriginal] = useState<boolean>(false) | ||||
| const [showConfirmDelete, setShowConfirmDelete] = useState(false) | const [showConfirmDelete, setShowConfirmDelete] = useState(false) | ||||
| const { new_app_id: newAppID } = await switchApp({ | const { new_app_id: newAppID } = await switchApp({ | ||||
| appID: appDetail.id, | appID: appDetail.id, | ||||
| name, | name, | ||||
| icon: emoji.icon, | |||||
| icon_background: emoji.icon_background, | |||||
| icon_type: appIcon.type, | |||||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||||
| icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, | |||||
| }) | }) | ||||
| if (onSuccess) | if (onSuccess) | ||||
| onSuccess() | onSuccess() | ||||
| <div className='pb-4'> | <div className='pb-4'> | ||||
| <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.switchLabel')}</div> | <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.switchLabel')}</div> | ||||
| <div className='flex items-center justify-between space-x-2'> | <div className='flex items-center justify-between space-x-2'> | ||||
| <AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> | |||||
| <AppIcon | |||||
| size='large' | |||||
| onClick={() => { setShowAppIconPicker(true) }} | |||||
| className='cursor-pointer' | |||||
| iconType={appIcon.type} | |||||
| icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon} | |||||
| background={appIcon.type === 'image' ? undefined : appIcon.background} | |||||
| imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} | |||||
| /> | |||||
| <input | <input | ||||
| value={name} | value={name} | ||||
| onChange={e => setName(e.target.value)} | onChange={e => setName(e.target.value)} | ||||
| className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs' | className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs' | ||||
| /> | /> | ||||
| </div> | </div> | ||||
| {showEmojiPicker && <EmojiPicker | |||||
| onSelect={(icon, icon_background) => { | |||||
| setEmoji({ icon, icon_background }) | |||||
| setShowEmojiPicker(false) | |||||
| {showAppIconPicker && <AppIconPicker | |||||
| onSelect={(payload) => { | |||||
| setAppIcon(payload) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| onClose={() => { | onClose={() => { | ||||
| setEmoji({ icon: appDetail.icon, icon_background: appDetail.icon_background }) | |||||
| setShowEmojiPicker(false) | |||||
| setAppIcon(appDetail.icon_type === 'image' | |||||
| ? { type: 'image' as const, url: appDetail.icon_url, fileId: appDetail.icon } | |||||
| : { type: 'emoji' as const, icon: appDetail.icon, background: appDetail.icon_background }) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| />} | />} | ||||
| </div> | </div> |
| 'use client' | |||||
| import type { ChangeEvent, FC } from 'react' | |||||
| import { createRef, useEffect, useState } from 'react' | |||||
| import type { Area } from 'react-easy-crop' | |||||
| import Cropper from 'react-easy-crop' | |||||
| import classNames from 'classnames' | |||||
| import { ImagePlus } from '../icons/src/vender/line/images' | |||||
| import { useDraggableUploader } from './hooks' | |||||
| import { ALLOW_FILE_EXTENSIONS } from '@/types/app' | |||||
| type UploaderProps = { | |||||
| className?: string | |||||
| onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void | |||||
| } | |||||
| const Uploader: FC<UploaderProps> = ({ | |||||
| className, | |||||
| onImageCropped, | |||||
| }) => { | |||||
| const [inputImage, setInputImage] = useState<{ file: File; url: string }>() | |||||
| useEffect(() => { | |||||
| return () => { | |||||
| if (inputImage) | |||||
| URL.revokeObjectURL(inputImage.url) | |||||
| } | |||||
| }, [inputImage]) | |||||
| const [crop, setCrop] = useState({ x: 0, y: 0 }) | |||||
| const [zoom, setZoom] = useState(1) | |||||
| const onCropComplete = async (_: Area, croppedAreaPixels: Area) => { | |||||
| if (!inputImage) | |||||
| return | |||||
| onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name) | |||||
| } | |||||
| const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => { | |||||
| const file = e.target.files?.[0] | |||||
| if (file) | |||||
| setInputImage({ file, url: URL.createObjectURL(file) }) | |||||
| } | |||||
| const { | |||||
| isDragActive, | |||||
| handleDragEnter, | |||||
| handleDragOver, | |||||
| handleDragLeave, | |||||
| handleDrop, | |||||
| } = useDraggableUploader((file: File) => setInputImage({ file, url: URL.createObjectURL(file) })) | |||||
| const inputRef = createRef<HTMLInputElement>() | |||||
| return ( | |||||
| <div className={classNames(className, 'w-full px-3 py-1.5')}> | |||||
| <div | |||||
| className={classNames( | |||||
| isDragActive && 'border-primary-600', | |||||
| 'relative aspect-square bg-gray-50 border-[1.5px] border-gray-200 border-dashed rounded-lg flex flex-col justify-center items-center text-gray-500')} | |||||
| onDragEnter={handleDragEnter} | |||||
| onDragOver={handleDragOver} | |||||
| onDragLeave={handleDragLeave} | |||||
| onDrop={handleDrop} | |||||
| > | |||||
| { | |||||
| !inputImage | |||||
| ? <> | |||||
| <ImagePlus className="w-[30px] h-[30px] mb-3 pointer-events-none" /> | |||||
| <div className="text-sm font-medium mb-[2px]"> | |||||
| <span className="pointer-events-none">Drop your image here, or </span> | |||||
| <button className="text-components-button-primary-bg" onClick={() => inputRef.current?.click()}>browse</button> | |||||
| <input | |||||
| ref={inputRef} type="file" className="hidden" | |||||
| onClick={e => ((e.target as HTMLInputElement).value = '')} | |||||
| accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} | |||||
| onChange={handleLocalFileInput} | |||||
| /> | |||||
| </div> | |||||
| <div className="text-xs pointer-events-none">Supports PNG, JPG, JPEG, WEBP and GIF</div> | |||||
| </> | |||||
| : <Cropper | |||||
| image={inputImage.url} | |||||
| crop={crop} | |||||
| zoom={zoom} | |||||
| aspect={1} | |||||
| onCropChange={setCrop} | |||||
| onCropComplete={onCropComplete} | |||||
| onZoomChange={setZoom} | |||||
| /> | |||||
| } | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default Uploader |
| import { useCallback, useState } from 'react' | |||||
| export const useDraggableUploader = <T extends HTMLElement>(setImageFn: (file: File) => void) => { | |||||
| const [isDragActive, setIsDragActive] = useState(false) | |||||
| const handleDragEnter = useCallback((e: React.DragEvent<T>) => { | |||||
| e.preventDefault() | |||||
| e.stopPropagation() | |||||
| setIsDragActive(true) | |||||
| }, []) | |||||
| const handleDragOver = useCallback((e: React.DragEvent<T>) => { | |||||
| e.preventDefault() | |||||
| e.stopPropagation() | |||||
| }, []) | |||||
| const handleDragLeave = useCallback((e: React.DragEvent<T>) => { | |||||
| e.preventDefault() | |||||
| e.stopPropagation() | |||||
| setIsDragActive(false) | |||||
| }, []) | |||||
| const handleDrop = useCallback((e: React.DragEvent<T>) => { | |||||
| e.preventDefault() | |||||
| e.stopPropagation() | |||||
| setIsDragActive(false) | |||||
| const file = e.dataTransfer.files[0] | |||||
| if (!file) | |||||
| return | |||||
| setImageFn(file) | |||||
| }, [setImageFn]) | |||||
| return { | |||||
| handleDragEnter, | |||||
| handleDragOver, | |||||
| handleDragLeave, | |||||
| handleDrop, | |||||
| isDragActive, | |||||
| } | |||||
| } |
| import type { FC } from 'react' | |||||
| import { useCallback, useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import type { Area } from 'react-easy-crop' | |||||
| import Modal from '../modal' | |||||
| import Divider from '../divider' | |||||
| import Button from '../button' | |||||
| import { ImagePlus } from '../icons/src/vender/line/images' | |||||
| import { useLocalFileUploader } from '../image-uploader/hooks' | |||||
| import EmojiPickerInner from '../emoji-picker/Inner' | |||||
| import Uploader from './Uploader' | |||||
| import s from './style.module.css' | |||||
| import getCroppedImg from './utils' | |||||
| import type { AppIconType, ImageFile } from '@/types/app' | |||||
| import cn from '@/utils/classnames' | |||||
| import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' | |||||
| export type AppIconEmojiSelection = { | |||||
| type: 'emoji' | |||||
| icon: string | |||||
| background: string | |||||
| } | |||||
| export type AppIconImageSelection = { | |||||
| type: 'image' | |||||
| fileId: string | |||||
| url: string | |||||
| } | |||||
| export type AppIconSelection = AppIconEmojiSelection | AppIconImageSelection | |||||
| type AppIconPickerProps = { | |||||
| onSelect?: (payload: AppIconSelection) => void | |||||
| onClose?: () => void | |||||
| className?: string | |||||
| } | |||||
| const AppIconPicker: FC<AppIconPickerProps> = ({ | |||||
| onSelect, | |||||
| onClose, | |||||
| className, | |||||
| }) => { | |||||
| const { t } = useTranslation() | |||||
| const tabs = [ | |||||
| { key: 'emoji', label: t('app.iconPicker.emoji'), icon: <span className="text-lg">🤖</span> }, | |||||
| { key: 'image', label: t('app.iconPicker.image'), icon: <ImagePlus /> }, | |||||
| ] | |||||
| const [activeTab, setActiveTab] = useState<AppIconType>('emoji') | |||||
| const [emoji, setEmoji] = useState<{ emoji: string; background: string }>() | |||||
| const handleSelectEmoji = useCallback((emoji: string, background: string) => { | |||||
| setEmoji({ emoji, background }) | |||||
| }, [setEmoji]) | |||||
| const [uploading, setUploading] = useState<boolean>() | |||||
| const { handleLocalFileUpload } = useLocalFileUploader({ | |||||
| limit: 3, | |||||
| disabled: false, | |||||
| onUpload: (imageFile: ImageFile) => { | |||||
| if (imageFile.fileId) { | |||||
| setUploading(false) | |||||
| onSelect?.({ | |||||
| type: 'image', | |||||
| fileId: imageFile.fileId, | |||||
| url: imageFile.url, | |||||
| }) | |||||
| } | |||||
| }, | |||||
| }) | |||||
| const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string; croppedAreaPixels: Area; fileName: string }>() | |||||
| const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => { | |||||
| setImageCropInfo({ tempUrl, croppedAreaPixels, fileName }) | |||||
| } | |||||
| const handleSelect = async () => { | |||||
| if (activeTab === 'emoji') { | |||||
| if (emoji) { | |||||
| onSelect?.({ | |||||
| type: 'emoji', | |||||
| icon: emoji.emoji, | |||||
| background: emoji.background, | |||||
| }) | |||||
| } | |||||
| } | |||||
| else { | |||||
| if (!imageCropInfo) | |||||
| return | |||||
| setUploading(true) | |||||
| const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels) | |||||
| const file = new File([blob], imageCropInfo.fileName, { type: blob.type }) | |||||
| handleLocalFileUpload(file) | |||||
| } | |||||
| } | |||||
| return <Modal | |||||
| onClose={() => { }} | |||||
| isShow | |||||
| closable={false} | |||||
| wrapperClassName={className} | |||||
| className={cn(s.container, '!w-[362px] !p-0')} | |||||
| > | |||||
| {!DISABLE_UPLOAD_IMAGE_AS_ICON && <div className="p-2 pb-0 w-full"> | |||||
| <div className='p-1 flex items-center justify-center gap-2 bg-background-body rounded-xl'> | |||||
| {tabs.map(tab => ( | |||||
| <button | |||||
| key={tab.key} | |||||
| className={` | |||||
| p-2 flex-1 flex justify-center items-center h-8 rounded-xl text-sm shrink-0 font-medium | |||||
| ${activeTab === tab.key && 'bg-components-main-nav-nav-button-bg-active shadow-md'} | |||||
| `} | |||||
| onClick={() => setActiveTab(tab.key as AppIconType)} | |||||
| > | |||||
| {tab.icon} {tab.label} | |||||
| </button> | |||||
| ))} | |||||
| </div> | |||||
| </div>} | |||||
| <Divider className='m-0' /> | |||||
| <EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} /> | |||||
| <Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} /> | |||||
| <Divider className='m-0' /> | |||||
| <div className='w-full flex items-center justify-center p-3 gap-2'> | |||||
| <Button className='w-full' onClick={() => onClose?.()}> | |||||
| {t('app.iconPicker.cancel')} | |||||
| </Button> | |||||
| <Button variant="primary" className='w-full' disabled={uploading} loading={uploading} onClick={handleSelect}> | |||||
| {t('app.iconPicker.ok')} | |||||
| </Button> | |||||
| </div> | |||||
| </Modal> | |||||
| } | |||||
| export default AppIconPicker |
| .container { | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: flex-start; | |||||
| width: 362px; | |||||
| max-height: 552px; | |||||
| border: 0.5px solid #EAECF0; | |||||
| box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); | |||||
| border-radius: 12px; | |||||
| background: #fff; | |||||
| } |
| export const createImage = (url: string) => | |||||
| new Promise<HTMLImageElement>((resolve, reject) => { | |||||
| const image = new Image() | |||||
| image.addEventListener('load', () => resolve(image)) | |||||
| image.addEventListener('error', error => reject(error)) | |||||
| image.setAttribute('crossOrigin', 'anonymous') // needed to avoid cross-origin issues on CodeSandbox | |||||
| image.src = url | |||||
| }) | |||||
| export function getRadianAngle(degreeValue: number) { | |||||
| return (degreeValue * Math.PI) / 180 | |||||
| } | |||||
| /** | |||||
| * Returns the new bounding area of a rotated rectangle. | |||||
| */ | |||||
| export function rotateSize(width: number, height: number, rotation: number) { | |||||
| const rotRad = getRadianAngle(rotation) | |||||
| return { | |||||
| width: | |||||
| Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height), | |||||
| height: | |||||
| Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height), | |||||
| } | |||||
| } | |||||
| /** | |||||
| * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop | |||||
| */ | |||||
| export default async function getCroppedImg( | |||||
| imageSrc: string, | |||||
| pixelCrop: { x: number; y: number; width: number; height: number }, | |||||
| rotation = 0, | |||||
| flip = { horizontal: false, vertical: false }, | |||||
| ): Promise<Blob> { | |||||
| const image = await createImage(imageSrc) | |||||
| const canvas = document.createElement('canvas') | |||||
| const ctx = canvas.getContext('2d') | |||||
| if (!ctx) | |||||
| throw new Error('Could not create a canvas context') | |||||
| const rotRad = getRadianAngle(rotation) | |||||
| // calculate bounding box of the rotated image | |||||
| const { width: bBoxWidth, height: bBoxHeight } = rotateSize( | |||||
| image.width, | |||||
| image.height, | |||||
| rotation, | |||||
| ) | |||||
| // set canvas size to match the bounding box | |||||
| canvas.width = bBoxWidth | |||||
| canvas.height = bBoxHeight | |||||
| // translate canvas context to a central location to allow rotating and flipping around the center | |||||
| ctx.translate(bBoxWidth / 2, bBoxHeight / 2) | |||||
| ctx.rotate(rotRad) | |||||
| ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1) | |||||
| ctx.translate(-image.width / 2, -image.height / 2) | |||||
| // draw rotated image | |||||
| ctx.drawImage(image, 0, 0) | |||||
| const croppedCanvas = document.createElement('canvas') | |||||
| const croppedCtx = croppedCanvas.getContext('2d') | |||||
| if (!croppedCtx) | |||||
| throw new Error('Could not create a canvas context') | |||||
| // Set the size of the cropped canvas | |||||
| croppedCanvas.width = pixelCrop.width | |||||
| croppedCanvas.height = pixelCrop.height | |||||
| // Draw the cropped image onto the new canvas | |||||
| croppedCtx.drawImage( | |||||
| canvas, | |||||
| pixelCrop.x, | |||||
| pixelCrop.y, | |||||
| pixelCrop.width, | |||||
| pixelCrop.height, | |||||
| 0, | |||||
| 0, | |||||
| pixelCrop.width, | |||||
| pixelCrop.height, | |||||
| ) | |||||
| return new Promise((resolve, reject) => { | |||||
| croppedCanvas.toBlob((file) => { | |||||
| if (file) | |||||
| resolve(file) | |||||
| else | |||||
| reject(new Error('Could not create a blob')) | |||||
| }, 'image/jpeg') | |||||
| }) | |||||
| } |
| import type { FC } from 'react' | |||||
| 'use client' | |||||
| import data from '@emoji-mart/data' | |||||
| import type { FC } from 'react' | |||||
| import { init } from 'emoji-mart' | import { init } from 'emoji-mart' | ||||
| import data from '@emoji-mart/data' | |||||
| import style from './style.module.css' | import style from './style.module.css' | ||||
| import classNames from '@/utils/classnames' | import classNames from '@/utils/classnames' | ||||
| import type { AppIconType } from '@/types/app' | |||||
| init({ data }) | init({ data }) | ||||
| export type AppIconProps = { | export type AppIconProps = { | ||||
| size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | ||||
| rounded?: boolean | rounded?: boolean | ||||
| iconType?: AppIconType | null | |||||
| icon?: string | icon?: string | ||||
| background?: string | |||||
| background?: string | null | |||||
| imageUrl?: string | null | |||||
| className?: string | className?: string | ||||
| innerIcon?: React.ReactNode | innerIcon?: React.ReactNode | ||||
| onClick?: () => void | onClick?: () => void | ||||
| const AppIcon: FC<AppIconProps> = ({ | const AppIcon: FC<AppIconProps> = ({ | ||||
| size = 'medium', | size = 'medium', | ||||
| rounded = false, | rounded = false, | ||||
| iconType, | |||||
| icon, | icon, | ||||
| background, | background, | ||||
| imageUrl, | |||||
| className, | className, | ||||
| innerIcon, | innerIcon, | ||||
| onClick, | onClick, | ||||
| }) => { | }) => { | ||||
| return ( | |||||
| <span | |||||
| className={classNames( | |||||
| style.appIcon, | |||||
| size !== 'medium' && style[size], | |||||
| rounded && style.rounded, | |||||
| className ?? '', | |||||
| )} | |||||
| style={{ | |||||
| background, | |||||
| }} | |||||
| onClick={onClick} | |||||
| > | |||||
| {innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />)} | |||||
| </span> | |||||
| const wrapperClassName = classNames( | |||||
| style.appIcon, | |||||
| size !== 'medium' && style[size], | |||||
| rounded && style.rounded, | |||||
| className ?? '', | |||||
| 'overflow-hidden', | |||||
| ) | ) | ||||
| const isValidImageIcon = iconType === 'image' && imageUrl | |||||
| return <span | |||||
| className={wrapperClassName} | |||||
| style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }} | |||||
| onClick={onClick} | |||||
| > | |||||
| {isValidImageIcon | |||||
| ? <img src={imageUrl} className="w-full h-full" alt="app icon" /> | |||||
| : (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />)) | |||||
| } | |||||
| </span> | |||||
| } | } | ||||
| export default AppIcon | export default AppIcon |
| .appIcon { | .appIcon { | ||||
| @apply flex items-center justify-center relative w-9 h-9 text-lg bg-teal-100 rounded-lg grow-0 shrink-0; | |||||
| @apply flex items-center justify-center relative w-9 h-9 text-lg rounded-lg grow-0 shrink-0; | |||||
| } | } | ||||
| .appIcon.large { | .appIcon.large { |
| const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) | const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) | ||||
| const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) | const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) | ||||
| useAppFavicon(!installedAppInfo, appInfo?.site.icon, appInfo?.site.icon_background) | |||||
| useAppFavicon({ | |||||
| enable: !installedAppInfo, | |||||
| icon_type: appInfo?.site.icon_type, | |||||
| icon: appInfo?.site.icon, | |||||
| icon_background: appInfo?.site.icon_background, | |||||
| icon_url: appInfo?.site.icon_url, | |||||
| }) | |||||
| const appData = useMemo(() => { | const appData = useMemo(() => { | ||||
| if (isInstalledApp) { | if (isInstalledApp) { | ||||
| app_id: id, | app_id: id, | ||||
| site: { | site: { | ||||
| title: app.name, | title: app.name, | ||||
| icon_type: app.icon_type, | |||||
| icon: app.icon, | icon: app.icon, | ||||
| icon_background: app.icon_background, | icon_background: app.icon_background, | ||||
| icon_url: app.icon_url, | |||||
| prompt_public: false, | prompt_public: false, | ||||
| copyright: '', | copyright: '', | ||||
| show_workflow_steps: true, | show_workflow_steps: true, |
| <AppIcon | <AppIcon | ||||
| className='mr-3' | className='mr-3' | ||||
| size='small' | size='small' | ||||
| iconType={appData?.site.icon_type} | |||||
| icon={appData?.site.icon} | icon={appData?.site.icon} | ||||
| background={appData?.site.icon_background} | background={appData?.site.icon_background} | ||||
| imageUrl={appData?.site.icon_url} | |||||
| /> | /> | ||||
| <div className='py-1 text-base font-semibold text-gray-800'> | <div className='py-1 text-base font-semibold text-gray-800'> | ||||
| {appData?.site.title} | {appData?.site.title} |
| 'use client' | |||||
| import type { ChangeEvent, FC } from 'react' | |||||
| import React, { useState } from 'react' | |||||
| import data from '@emoji-mart/data' | |||||
| import type { EmojiMartData } from '@emoji-mart/data' | |||||
| import { init } from 'emoji-mart' | |||||
| import { | |||||
| MagnifyingGlassIcon, | |||||
| } from '@heroicons/react/24/outline' | |||||
| import cn from '@/utils/classnames' | |||||
| import Divider from '@/app/components/base/divider' | |||||
| import { searchEmoji } from '@/utils/emoji' | |||||
| declare global { | |||||
| namespace JSX { | |||||
| // eslint-disable-next-line @typescript-eslint/consistent-type-definitions | |||||
| interface IntrinsicElements { | |||||
| 'em-emoji': React.DetailedHTMLProps< React.HTMLAttributes<HTMLElement>, HTMLElement > | |||||
| } | |||||
| } | |||||
| } | |||||
| init({ data }) | |||||
| const backgroundColors = [ | |||||
| '#FFEAD5', | |||||
| '#E4FBCC', | |||||
| '#D3F8DF', | |||||
| '#E0F2FE', | |||||
| '#E0EAFF', | |||||
| '#EFF1F5', | |||||
| '#FBE8FF', | |||||
| '#FCE7F6', | |||||
| '#FEF7C3', | |||||
| '#E6F4D7', | |||||
| '#D5F5F6', | |||||
| '#D1E9FF', | |||||
| '#D1E0FF', | |||||
| '#D5D9EB', | |||||
| '#ECE9FE', | |||||
| '#FFE4E8', | |||||
| ] | |||||
| type IEmojiPickerInnerProps = { | |||||
| emoji?: string | |||||
| background?: string | |||||
| onSelect?: (emoji: string, background: string) => void | |||||
| className?: string | |||||
| } | |||||
| const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({ | |||||
| onSelect, | |||||
| className, | |||||
| }) => { | |||||
| const { categories } = data as EmojiMartData | |||||
| const [selectedEmoji, setSelectedEmoji] = useState('') | |||||
| const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0]) | |||||
| const [searchedEmojis, setSearchedEmojis] = useState<string[]>([]) | |||||
| const [isSearching, setIsSearching] = useState(false) | |||||
| React.useEffect(() => { | |||||
| if (selectedEmoji && selectedBackground) | |||||
| onSelect?.(selectedEmoji, selectedBackground) | |||||
| }, [onSelect, selectedEmoji, selectedBackground]) | |||||
| return <div className={cn(className)}> | |||||
| <div className='flex flex-col items-center w-full px-3'> | |||||
| <div className="relative w-full"> | |||||
| <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> | |||||
| <MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> | |||||
| </div> | |||||
| <input | |||||
| type="search" | |||||
| id="search" | |||||
| className='block w-full h-10 px-3 pl-10 text-sm font-normal bg-gray-100 rounded-lg' | |||||
| placeholder="Search emojis..." | |||||
| onChange={async (e: ChangeEvent<HTMLInputElement>) => { | |||||
| if (e.target.value === '') { | |||||
| setIsSearching(false) | |||||
| } | |||||
| else { | |||||
| setIsSearching(true) | |||||
| const emojis = await searchEmoji(e.target.value) | |||||
| setSearchedEmojis(emojis) | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| <Divider className='m-0 mb-3' /> | |||||
| <div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3"> | |||||
| {isSearching && <> | |||||
| <div key={'category-search'} className='flex flex-col'> | |||||
| <p className='font-medium uppercase text-xs text-[#101828] mb-1'>Search</p> | |||||
| <div className='w-full h-full grid grid-cols-8 gap-1'> | |||||
| {searchedEmojis.map((emoji: string, index: number) => { | |||||
| return <div | |||||
| key={`emoji-search-${index}`} | |||||
| className='inline-flex w-10 h-10 rounded-lg items-center justify-center' | |||||
| onClick={() => { | |||||
| setSelectedEmoji(emoji) | |||||
| }} | |||||
| > | |||||
| <div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'> | |||||
| <em-emoji id={emoji} /> | |||||
| </div> | |||||
| </div> | |||||
| })} | |||||
| </div> | |||||
| </div> | |||||
| </>} | |||||
| {categories.map((category, index: number) => { | |||||
| return <div key={`category-${index}`} className='flex flex-col'> | |||||
| <p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p> | |||||
| <div className='w-full h-full grid grid-cols-8 gap-1'> | |||||
| {category.emojis.map((emoji, index: number) => { | |||||
| return <div | |||||
| key={`emoji-${index}`} | |||||
| className='inline-flex w-10 h-10 rounded-lg items-center justify-center' | |||||
| onClick={() => { | |||||
| setSelectedEmoji(emoji) | |||||
| }} | |||||
| > | |||||
| <div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'> | |||||
| <em-emoji id={emoji} /> | |||||
| </div> | |||||
| </div> | |||||
| })} | |||||
| </div> | |||||
| </div> | |||||
| })} | |||||
| </div> | |||||
| {/* Color Select */} | |||||
| <div className={cn('p-3 pb-0', selectedEmoji === '' ? 'opacity-25' : '')}> | |||||
| <p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p> | |||||
| <div className='w-full h-full grid grid-cols-8 gap-1'> | |||||
| {backgroundColors.map((color) => { | |||||
| return <div | |||||
| key={color} | |||||
| className={ | |||||
| cn( | |||||
| 'cursor-pointer', | |||||
| 'hover:ring-1 ring-offset-1', | |||||
| 'inline-flex w-10 h-10 rounded-lg items-center justify-center', | |||||
| color === selectedBackground ? 'ring-1 ring-gray-300' : '', | |||||
| )} | |||||
| onClick={() => { | |||||
| setSelectedBackground(color) | |||||
| }} | |||||
| > | |||||
| <div className={cn( | |||||
| 'w-8 h-8 p-1 flex items-center justify-center rounded-lg', | |||||
| ) | |||||
| } style={{ background: color }}> | |||||
| {selectedEmoji !== '' && <em-emoji id={selectedEmoji} />} | |||||
| </div> | |||||
| </div> | |||||
| })} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| } | |||||
| export default EmojiPickerInner |
| /* eslint-disable multiline-ternary */ | |||||
| 'use client' | 'use client' | ||||
| import type { ChangeEvent, FC } from 'react' | |||||
| import React, { useState } from 'react' | |||||
| import data from '@emoji-mart/data' | |||||
| import type { EmojiMartData } from '@emoji-mart/data' | |||||
| import { init } from 'emoji-mart' | |||||
| import { | |||||
| MagnifyingGlassIcon, | |||||
| } from '@heroicons/react/24/outline' | |||||
| import type { FC } from 'react' | |||||
| import React, { useCallback, useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import s from './style.module.css' | import s from './style.module.css' | ||||
| import EmojiPickerInner from './Inner' | |||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import Divider from '@/app/components/base/divider' | import Divider from '@/app/components/base/divider' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import Modal from '@/app/components/base/modal' | import Modal from '@/app/components/base/modal' | ||||
| import { searchEmoji } from '@/utils/emoji' | |||||
| declare global { | |||||
| namespace JSX { | |||||
| // eslint-disable-next-line @typescript-eslint/consistent-type-definitions | |||||
| interface IntrinsicElements { | |||||
| 'em-emoji': React.DetailedHTMLProps< | |||||
| React.HTMLAttributes<HTMLElement>, | |||||
| HTMLElement | |||||
| > | |||||
| } | |||||
| } | |||||
| } | |||||
| init({ data }) | |||||
| const backgroundColors = [ | |||||
| '#FFEAD5', | |||||
| '#E4FBCC', | |||||
| '#D3F8DF', | |||||
| '#E0F2FE', | |||||
| '#E0EAFF', | |||||
| '#EFF1F5', | |||||
| '#FBE8FF', | |||||
| '#FCE7F6', | |||||
| '#FEF7C3', | |||||
| '#E6F4D7', | |||||
| '#D5F5F6', | |||||
| '#D1E9FF', | |||||
| '#D1E0FF', | |||||
| '#D5D9EB', | |||||
| '#ECE9FE', | |||||
| '#FFE4E8', | |||||
| ] | |||||
| type IEmojiPickerProps = { | type IEmojiPickerProps = { | ||||
| isModal?: boolean | isModal?: boolean | ||||
| className, | className, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { categories } = data as EmojiMartData | |||||
| const [selectedEmoji, setSelectedEmoji] = useState('') | const [selectedEmoji, setSelectedEmoji] = useState('') | ||||
| const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0]) | |||||
| const [selectedBackground, setSelectedBackground] = useState<string>() | |||||
| const [searchedEmojis, setSearchedEmojis] = useState<string[]>([]) | |||||
| const [isSearching, setIsSearching] = useState(false) | |||||
| const handleSelectEmoji = useCallback((emoji: string, background: string) => { | |||||
| setSelectedEmoji(emoji) | |||||
| setSelectedBackground(background) | |||||
| }, [setSelectedEmoji, setSelectedBackground]) | |||||
| return isModal ? <Modal | |||||
| onClose={() => { }} | |||||
| isShow | |||||
| closable={false} | |||||
| wrapperClassName={className} | |||||
| className={cn(s.container, '!w-[362px] !p-0')} | |||||
| > | |||||
| <div className='flex flex-col items-center w-full p-3'> | |||||
| <div className="relative w-full"> | |||||
| <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> | |||||
| <MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> | |||||
| </div> | |||||
| <input | |||||
| type="search" | |||||
| id="search" | |||||
| className='block w-full h-10 px-3 pl-10 text-sm font-normal bg-gray-100 rounded-lg' | |||||
| placeholder="Search emojis..." | |||||
| onChange={async (e: ChangeEvent<HTMLInputElement>) => { | |||||
| if (e.target.value === '') { | |||||
| setIsSearching(false) | |||||
| } | |||||
| else { | |||||
| setIsSearching(true) | |||||
| const emojis = await searchEmoji(e.target.value) | |||||
| setSearchedEmojis(emojis) | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| <Divider className='m-0 mb-3' /> | |||||
| <div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3"> | |||||
| {isSearching && <> | |||||
| <div key={'category-search'} className='flex flex-col'> | |||||
| <p className='font-medium uppercase text-xs text-[#101828] mb-1'>Search</p> | |||||
| <div className='w-full h-full grid grid-cols-8 gap-1'> | |||||
| {searchedEmojis.map((emoji: string, index: number) => { | |||||
| return <div | |||||
| key={`emoji-search-${index}`} | |||||
| className='inline-flex w-10 h-10 rounded-lg items-center justify-center' | |||||
| onClick={() => { | |||||
| setSelectedEmoji(emoji) | |||||
| }} | |||||
| > | |||||
| <div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'> | |||||
| <em-emoji id={emoji} /> | |||||
| </div> | |||||
| </div> | |||||
| })} | |||||
| </div> | |||||
| </div> | |||||
| </>} | |||||
| {categories.map((category, index: number) => { | |||||
| return <div key={`category-${index}`} className='flex flex-col'> | |||||
| <p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p> | |||||
| <div className='w-full h-full grid grid-cols-8 gap-1'> | |||||
| {category.emojis.map((emoji, index: number) => { | |||||
| return <div | |||||
| key={`emoji-${index}`} | |||||
| className='inline-flex w-10 h-10 rounded-lg items-center justify-center' | |||||
| onClick={() => { | |||||
| setSelectedEmoji(emoji) | |||||
| }} | |||||
| > | |||||
| <div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'> | |||||
| <em-emoji id={emoji} /> | |||||
| </div> | |||||
| </div> | |||||
| })} | |||||
| </div> | |||||
| </div> | |||||
| })} | |||||
| </div> | |||||
| {/* Color Select */} | |||||
| <div className={cn('p-3 ', selectedEmoji === '' ? 'opacity-25' : '')}> | |||||
| <p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p> | |||||
| <div className='w-full h-full grid grid-cols-8 gap-1'> | |||||
| {backgroundColors.map((color) => { | |||||
| return <div | |||||
| key={color} | |||||
| className={ | |||||
| cn( | |||||
| 'cursor-pointer', | |||||
| 'hover:ring-1 ring-offset-1', | |||||
| 'inline-flex w-10 h-10 rounded-lg items-center justify-center', | |||||
| color === selectedBackground ? 'ring-1 ring-gray-300' : '', | |||||
| )} | |||||
| onClick={() => { | |||||
| setSelectedBackground(color) | |||||
| }} | |||||
| > | |||||
| <div className={cn( | |||||
| 'w-8 h-8 p-1 flex items-center justify-center rounded-lg', | |||||
| ) | |||||
| } style={{ background: color }}> | |||||
| {selectedEmoji !== '' && <em-emoji id={selectedEmoji} />} | |||||
| </div> | |||||
| </div> | |||||
| })} | |||||
| </div> | |||||
| </div> | |||||
| <Divider className='m-0' /> | |||||
| <div className='w-full flex items-center justify-center p-3 gap-2'> | |||||
| <Button className='w-full' onClick={() => { | |||||
| onClose && onClose() | |||||
| }}> | |||||
| {t('app.emoji.cancel')} | |||||
| </Button> | |||||
| <Button | |||||
| disabled={selectedEmoji === ''} | |||||
| variant="primary" | |||||
| className='w-full' | |||||
| onClick={() => { | |||||
| onSelect && onSelect(selectedEmoji, selectedBackground) | |||||
| return isModal | |||||
| ? <Modal | |||||
| onClose={() => { }} | |||||
| isShow | |||||
| closable={false} | |||||
| wrapperClassName={className} | |||||
| className={cn(s.container, '!w-[362px] !p-0')} | |||||
| > | |||||
| <EmojiPickerInner | |||||
| className="pt-3" | |||||
| onSelect={handleSelectEmoji} /> | |||||
| <Divider className='m-0' /> | |||||
| <div className='w-full flex items-center justify-center p-3 gap-2'> | |||||
| <Button className='w-full' onClick={() => { | |||||
| onClose && onClose() | |||||
| }}> | }}> | ||||
| {t('app.emoji.ok')} | |||||
| </Button> | |||||
| </div> | |||||
| </Modal> : <> | |||||
| </> | |||||
| {t('app.iconPicker.cancel')} | |||||
| </Button> | |||||
| <Button | |||||
| disabled={selectedEmoji === '' || !selectedBackground} | |||||
| variant="primary" | |||||
| className='w-full' | |||||
| onClick={() => { | |||||
| onSelect && onSelect(selectedEmoji, selectedBackground!) | |||||
| }}> | |||||
| {t('app.iconPicker.ok')} | |||||
| </Button> | |||||
| </div> | |||||
| </Modal> | |||||
| : <></> | |||||
| } | } | ||||
| export default EmojiPicker | export default EmojiPicker |
| <div className={cn('group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg')}> | <div className={cn('group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg')}> | ||||
| <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'> | <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'> | ||||
| <div className='relative shrink-0'> | <div className='relative shrink-0'> | ||||
| <AppIcon size='large' icon={app.app.icon} background={app.app.icon_background} /> | |||||
| <AppIcon | |||||
| size='large' | |||||
| iconType={app.app.icon_type} | |||||
| icon={app.app.icon} | |||||
| background={app.app.icon_background} | |||||
| imageUrl={app.app.icon_url} | |||||
| /> | |||||
| <span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'> | <span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'> | ||||
| {appBasicInfo.mode === 'advanced-chat' && ( | {appBasicInfo.mode === 'advanced-chat' && ( | ||||
| <ChatBot className='w-3 h-3 text-[#1570EF]' /> | <ChatBot className='w-3 h-3 text-[#1570EF]' /> |
| const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) | const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) | ||||
| const onCreate: CreateAppModalProps['onConfirm'] = async ({ | const onCreate: CreateAppModalProps['onConfirm'] = async ({ | ||||
| name, | name, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| description, | description, | ||||
| const app = await importApp({ | const app = await importApp({ | ||||
| data: export_data, | data: export_data, | ||||
| name, | name, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| description, | description, | ||||
| </div> | </div> | ||||
| {isShowCreateModal && ( | {isShowCreateModal && ( | ||||
| <CreateAppModal | <CreateAppModal | ||||
| appIconType={currApp?.app.icon_type || 'emoji'} | |||||
| appIcon={currApp?.app.icon || ''} | appIcon={currApp?.app.icon || ''} | ||||
| appIconBackground={currApp?.app.icon_background || ''} | appIconBackground={currApp?.app.icon_background || ''} | ||||
| appIconUrl={currApp?.app.icon_url} | |||||
| appName={currApp?.app.name || ''} | appName={currApp?.app.name || ''} | ||||
| appDescription={currApp?.app.description || ''} | appDescription={currApp?.app.description || ''} | ||||
| show={isShowCreateModal} | show={isShowCreateModal} |
| import React, { useState } from 'react' | import React, { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { RiCloseLine } from '@remixicon/react' | import { RiCloseLine } from '@remixicon/react' | ||||
| import AppIconPicker from '../../base/app-icon-picker' | |||||
| import Modal from '@/app/components/base/modal' | import Modal from '@/app/components/base/modal' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import EmojiPicker from '@/app/components/base/emoji-picker' | |||||
| import { useProviderContext } from '@/context/provider-context' | import { useProviderContext } from '@/context/provider-context' | ||||
| import AppsFull from '@/app/components/billing/apps-full-in-dialog' | import AppsFull from '@/app/components/billing/apps-full-in-dialog' | ||||
| import type { AppIconType } from '@/types/app' | |||||
| export type CreateAppModalProps = { | export type CreateAppModalProps = { | ||||
| show: boolean | show: boolean | ||||
| isEditModal?: boolean | isEditModal?: boolean | ||||
| appName: string | appName: string | ||||
| appDescription: string | appDescription: string | ||||
| appIconType: AppIconType | null | |||||
| appIcon: string | appIcon: string | ||||
| appIconBackground: string | |||||
| appIconBackground?: string | null | |||||
| appIconUrl?: string | null | |||||
| onConfirm: (info: { | onConfirm: (info: { | ||||
| name: string | name: string | ||||
| icon_type: AppIconType | |||||
| icon: string | icon: string | ||||
| icon_background: string | |||||
| icon_background?: string | |||||
| description: string | description: string | ||||
| }) => Promise<void> | }) => Promise<void> | ||||
| onHide: () => void | onHide: () => void | ||||
| const CreateAppModal = ({ | const CreateAppModal = ({ | ||||
| show = false, | show = false, | ||||
| isEditModal = false, | isEditModal = false, | ||||
| appIcon, | |||||
| appIconType, | |||||
| appIcon: _appIcon, | |||||
| appIconBackground, | appIconBackground, | ||||
| appIconUrl, | |||||
| appName, | appName, | ||||
| appDescription, | appDescription, | ||||
| onConfirm, | onConfirm, | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const [name, setName] = React.useState(appName) | const [name, setName] = React.useState(appName) | ||||
| const [showEmojiPicker, setShowEmojiPicker] = useState(false) | |||||
| const [emoji, setEmoji] = useState({ icon: appIcon, icon_background: appIconBackground }) | |||||
| const [appIcon, setAppIcon] = useState( | |||||
| () => appIconType === 'image' | |||||
| ? { type: 'image' as const, fileId: _appIcon, url: appIconUrl } | |||||
| : { type: 'emoji' as const, icon: _appIcon, background: appIconBackground }, | |||||
| ) | |||||
| const [showAppIconPicker, setShowAppIconPicker] = useState(false) | |||||
| const [description, setDescription] = useState(appDescription || '') | const [description, setDescription] = useState(appDescription || '') | ||||
| const { plan, enableBilling } = useProviderContext() | const { plan, enableBilling } = useProviderContext() | ||||
| } | } | ||||
| onConfirm({ | onConfirm({ | ||||
| name, | name, | ||||
| ...emoji, | |||||
| icon_type: appIcon.type, | |||||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||||
| icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined, | |||||
| description, | description, | ||||
| }) | }) | ||||
| onHide() | onHide() | ||||
| <div className='pt-2'> | <div className='pt-2'> | ||||
| <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionName')}</div> | <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionName')}</div> | ||||
| <div className='flex items-center justify-between space-x-2'> | <div className='flex items-center justify-between space-x-2'> | ||||
| <AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> | |||||
| <AppIcon | |||||
| size='large' | |||||
| onClick={() => { setShowAppIconPicker(true) }} | |||||
| className='cursor-pointer' | |||||
| iconType={appIcon.type} | |||||
| icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon} | |||||
| background={appIcon.type === 'image' ? undefined : appIcon.background} | |||||
| imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} | |||||
| /> | |||||
| <input | <input | ||||
| value={name} | value={name} | ||||
| onChange={e => setName(e.target.value)} | onChange={e => setName(e.target.value)} | ||||
| <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button> | <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button> | ||||
| </div> | </div> | ||||
| </Modal> | </Modal> | ||||
| {showEmojiPicker && <EmojiPicker | |||||
| onSelect={(icon, icon_background) => { | |||||
| setEmoji({ icon, icon_background }) | |||||
| setShowEmojiPicker(false) | |||||
| {showAppIconPicker && <AppIconPicker | |||||
| onSelect={(payload) => { | |||||
| setAppIcon(payload) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| onClose={() => { | onClose={() => { | ||||
| setEmoji({ icon: appIcon, icon_background: appIconBackground }) | |||||
| setShowEmojiPicker(false) | |||||
| setAppIcon(appIconType === 'image' | |||||
| ? { type: 'image' as const, url: appIconUrl, fileId: _appIcon } | |||||
| : { type: 'emoji' as const, icon: _appIcon, background: appIconBackground }) | |||||
| setShowAppIconPicker(false) | |||||
| }} | }} | ||||
| />} | />} | ||||
| </> | </> | ||||
| ) | ) | ||||
| } | } | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import ItemOperation from '@/app/components/explore/item-operation' | import ItemOperation from '@/app/components/explore/item-operation' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import type { AppIconType } from '@/types/app' | |||||
| export type IAppNavItemProps = { | export type IAppNavItemProps = { | ||||
| isMobile: boolean | isMobile: boolean | ||||
| name: string | name: string | ||||
| id: string | id: string | ||||
| icon_type: AppIconType | null | |||||
| icon: string | icon: string | ||||
| icon_background: string | icon_background: string | ||||
| icon_url: string | |||||
| isSelected: boolean | isSelected: boolean | ||||
| isPinned: boolean | isPinned: boolean | ||||
| togglePin: () => void | togglePin: () => void | ||||
| isMobile, | isMobile, | ||||
| name, | name, | ||||
| id, | id, | ||||
| icon_type, | |||||
| icon, | icon, | ||||
| icon_background, | icon_background, | ||||
| icon_url, | |||||
| isSelected, | isSelected, | ||||
| isPinned, | isPinned, | ||||
| togglePin, | togglePin, | ||||
| router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation(). | router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation(). | ||||
| }} | }} | ||||
| > | > | ||||
| {isMobile && <AppIcon size='tiny' icon={icon} background={icon_background} />} | |||||
| {isMobile && <AppIcon size='tiny' iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} />} | |||||
| {!isMobile && ( | {!isMobile && ( | ||||
| <> | <> | ||||
| <div className='flex items-center space-x-2 w-0 grow'> | <div className='flex items-center space-x-2 w-0 grow'> | ||||
| <AppIcon size='tiny' icon={icon} background={icon_background} /> | |||||
| <AppIcon size='tiny' iconType={icon_type} icon={icon} background={icon_background} imageUrl={icon_url} /> | |||||
| <div className='overflow-hidden text-ellipsis whitespace-nowrap' title={name}>{name}</div> | <div className='overflow-hidden text-ellipsis whitespace-nowrap' title={name}>{name}</div> | ||||
| </div> | </div> | ||||
| <div className='shrink-0 h-6' onClick={e => e.stopPropagation()}> | <div className='shrink-0 h-6' onClick={e => e.stopPropagation()}> |
| height: 'calc(100vh - 250px)', | height: 'calc(100vh - 250px)', | ||||
| }} | }} | ||||
| > | > | ||||
| {installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon, icon_background } }) => { | |||||
| {installedApps.map(({ id, is_pinned, uninstallable, app: { name, icon_type, icon, icon_url, icon_background } }) => { | |||||
| return ( | return ( | ||||
| <Item | <Item | ||||
| key={id} | key={id} | ||||
| isMobile={isMobile} | isMobile={isMobile} | ||||
| name={name} | name={name} | ||||
| icon_type={icon_type} | |||||
| icon={icon} | icon={icon} | ||||
| icon_background={icon_background} | icon_background={icon_background} | ||||
| icon_url={icon_url} | |||||
| id={id} | id={id} | ||||
| isSelected={lastSegment?.toLowerCase() === id} | isSelected={lastSegment?.toLowerCase() === id} | ||||
| isPinned={is_pinned} | isPinned={is_pinned} |
| } | } | ||||
| }, [siteInfo?.title, canReplaceLogo]) | }, [siteInfo?.title, canReplaceLogo]) | ||||
| useAppFavicon(!isInstalledApp, siteInfo?.icon, siteInfo?.icon_background) | |||||
| useAppFavicon({ | |||||
| enable: !isInstalledApp, | |||||
| icon_type: siteInfo?.icon_type, | |||||
| icon: siteInfo?.icon, | |||||
| icon_background: siteInfo?.icon_background, | |||||
| icon_url: siteInfo?.icon_url, | |||||
| }) | |||||
| const [isShowResSidebar, { setTrue: doShowResSidebar, setFalse: hideResSidebar }] = useBoolean(false) | const [isShowResSidebar, { setTrue: doShowResSidebar, setFalse: hideResSidebar }] = useBoolean(false) | ||||
| const showResSidebar = () => { | const showResSidebar = () => { |
| export const VAR_REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi | export const VAR_REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi | ||||
| export const TEXT_GENERATION_TIMEOUT_MS = 60000 | export const TEXT_GENERATION_TIMEOUT_MS = 60000 | ||||
| export const DISABLE_UPLOAD_IMAGE_AS_ICON = process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true' |
| import { useAsyncEffect } from 'ahooks' | import { useAsyncEffect } from 'ahooks' | ||||
| import { appDefaultIconBackground } from '@/config' | import { appDefaultIconBackground } from '@/config' | ||||
| import { searchEmoji } from '@/utils/emoji' | import { searchEmoji } from '@/utils/emoji' | ||||
| import type { AppIconType } from '@/types/app' | |||||
| type UseAppFaviconOptions = { | |||||
| enable?: boolean | |||||
| icon_type?: AppIconType | |||||
| icon?: string | |||||
| icon_background?: string | |||||
| icon_url?: string | |||||
| } | |||||
| export function useAppFavicon(options: UseAppFaviconOptions) { | |||||
| const { | |||||
| enable = true, | |||||
| icon_type = 'emoji', | |||||
| icon, | |||||
| icon_background, | |||||
| icon_url, | |||||
| } = options | |||||
| export function useAppFavicon(enable: boolean, icon?: string, icon_background?: string) { | |||||
| useAsyncEffect(async () => { | useAsyncEffect(async () => { | ||||
| if (!enable) | if (!enable) | ||||
| return | return | ||||
| const isValidImageIcon = icon_type === 'image' && icon_url | |||||
| const link: HTMLLinkElement = document.querySelector('link[rel*="icon"]') || document.createElement('link') | const link: HTMLLinkElement = document.querySelector('link[rel*="icon"]') || document.createElement('link') | ||||
| // eslint-disable-next-line prefer-template | |||||
| link.href = 'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22>' | |||||
| + '<rect width=%22100%25%22 height=%22100%25%22 fill=%22' + encodeURIComponent(icon_background || appDefaultIconBackground) + '%22 rx=%2230%22 ry=%2230%22 />' | |||||
| + '<text x=%2212.5%22 y=%221em%22 font-size=%2275%22>' | |||||
| + (icon ? await searchEmoji(icon) : '🤖') | |||||
| + '</text>' | |||||
| link.href = isValidImageIcon | |||||
| ? icon_url | |||||
| : 'data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22>' | |||||
| + `<rect width=%22100%25%22 height=%22100%25%22 fill=%22${encodeURIComponent(icon_background || appDefaultIconBackground)}%22 rx=%2230%22 ry=%2230%22 />` | |||||
| + `<text x=%2212.5%22 y=%221em%22 font-size=%2275%22>${ | |||||
| icon ? await searchEmoji(icon) : '🤖' | |||||
| }</text>` | |||||
| + '</svg>' | + '</svg>' | ||||
| link.rel = 'shortcut icon' | link.rel = 'shortcut icon' |
| editAppTitle: 'App-Informationen bearbeiten', | editAppTitle: 'App-Informationen bearbeiten', | ||||
| editDone: 'App-Informationen wurden aktualisiert', | editDone: 'App-Informationen wurden aktualisiert', | ||||
| editFailed: 'Aktualisierung der App-Informationen fehlgeschlagen', | editFailed: 'Aktualisierung der App-Informationen fehlgeschlagen', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Abbrechen', | cancel: 'Abbrechen', | ||||
| }, | }, |
| editAppTitle: 'Edit App Info', | editAppTitle: 'Edit App Info', | ||||
| editDone: 'App info updated', | editDone: 'App info updated', | ||||
| editFailed: 'Failed to update app info', | editFailed: 'Failed to update app info', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Cancel', | cancel: 'Cancel', | ||||
| emoji: 'Emoji', | |||||
| image: 'Image', | |||||
| }, | }, | ||||
| switch: 'Switch to Workflow Orchestrate', | switch: 'Switch to Workflow Orchestrate', | ||||
| switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ', | switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ', |
| editAppTitle: 'Editar información de la app', | editAppTitle: 'Editar información de la app', | ||||
| editDone: 'Información de la app actualizada', | editDone: 'Información de la app actualizada', | ||||
| editFailed: 'Error al actualizar información de la app', | editFailed: 'Error al actualizar información de la app', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Cancelar', | cancel: 'Cancelar', | ||||
| }, | }, |
| editAppTitle: 'ویرایش اطلاعات برنامه', | editAppTitle: 'ویرایش اطلاعات برنامه', | ||||
| editDone: 'اطلاعات برنامه بهروزرسانی شد', | editDone: 'اطلاعات برنامه بهروزرسانی شد', | ||||
| editFailed: 'بهروزرسانی اطلاعات برنامه ناموفق بود', | editFailed: 'بهروزرسانی اطلاعات برنامه ناموفق بود', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'باشه', | ok: 'باشه', | ||||
| cancel: 'لغو', | cancel: 'لغو', | ||||
| }, | }, |
| editAppTitle: 'Modifier les informations de l\'application', | editAppTitle: 'Modifier les informations de l\'application', | ||||
| editDone: 'Informations sur l\'application mises à jour', | editDone: 'Informations sur l\'application mises à jour', | ||||
| editFailed: 'Échec de la mise à jour des informations de l\'application', | editFailed: 'Échec de la mise à jour des informations de l\'application', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Annuler', | cancel: 'Annuler', | ||||
| }, | }, |
| editAppTitle: 'ऐप जानकारी संपादित करें', | editAppTitle: 'ऐप जानकारी संपादित करें', | ||||
| editDone: 'ऐप जानकारी अपडेट की गई', | editDone: 'ऐप जानकारी अपडेट की गई', | ||||
| editFailed: 'ऐप जानकारी अपडेट करने में विफल', | editFailed: 'ऐप जानकारी अपडेट करने में विफल', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'ठीक है', | ok: 'ठीक है', | ||||
| cancel: 'रद्द करें', | cancel: 'रद्द करें', | ||||
| }, | }, |
| editAppTitle: 'Modifica Info App', | editAppTitle: 'Modifica Info App', | ||||
| editDone: 'Info app aggiornata', | editDone: 'Info app aggiornata', | ||||
| editFailed: 'Aggiornamento delle info dell\'app fallito', | editFailed: 'Aggiornamento delle info dell\'app fallito', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Annulla', | cancel: 'Annulla', | ||||
| }, | }, |
| editAppTitle: 'アプリ情報を編集する', | editAppTitle: 'アプリ情報を編集する', | ||||
| editDone: 'アプリ情報が更新されました', | editDone: 'アプリ情報が更新されました', | ||||
| editFailed: 'アプリ情報の更新に失敗しました', | editFailed: 'アプリ情報の更新に失敗しました', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'キャンセル', | cancel: 'キャンセル', | ||||
| }, | }, |
| editAppTitle: '앱 정보 편집하기', | editAppTitle: '앱 정보 편집하기', | ||||
| editDone: '앱 정보가 업데이트되었습니다', | editDone: '앱 정보가 업데이트되었습니다', | ||||
| editFailed: '앱 정보 업데이트 실패', | editFailed: '앱 정보 업데이트 실패', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: '확인', | ok: '확인', | ||||
| cancel: '취소', | cancel: '취소', | ||||
| }, | }, |
| editAppTitle: 'Edytuj informacje o aplikacji', | editAppTitle: 'Edytuj informacje o aplikacji', | ||||
| editDone: 'Informacje o aplikacji zaktualizowane', | editDone: 'Informacje o aplikacji zaktualizowane', | ||||
| editFailed: 'Nie udało się zaktualizować informacji o aplikacji', | editFailed: 'Nie udało się zaktualizować informacji o aplikacji', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Anuluj', | cancel: 'Anuluj', | ||||
| }, | }, |
| editAppTitle: 'Editar Informações do Aplicativo', | editAppTitle: 'Editar Informações do Aplicativo', | ||||
| editDone: 'Informações do aplicativo atualizadas', | editDone: 'Informações do aplicativo atualizadas', | ||||
| editFailed: 'Falha ao atualizar informações do aplicativo', | editFailed: 'Falha ao atualizar informações do aplicativo', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Cancelar', | cancel: 'Cancelar', | ||||
| }, | }, |
| editAppTitle: 'Editează Info Aplicație', | editAppTitle: 'Editează Info Aplicație', | ||||
| editDone: 'Informațiile despre aplicație au fost actualizate', | editDone: 'Informațiile despre aplicație au fost actualizate', | ||||
| editFailed: 'Actualizarea informațiilor despre aplicație a eșuat', | editFailed: 'Actualizarea informațiilor despre aplicație a eșuat', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Anulează', | cancel: 'Anulează', | ||||
| }, | }, |
| editAppTitle: 'Uygulama Bilgilerini Düzenle', | editAppTitle: 'Uygulama Bilgilerini Düzenle', | ||||
| editDone: 'Uygulama bilgileri güncellendi', | editDone: 'Uygulama bilgileri güncellendi', | ||||
| editFailed: 'Uygulama bilgileri güncellenemedi', | editFailed: 'Uygulama bilgileri güncellenemedi', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'Tamam', | ok: 'Tamam', | ||||
| cancel: 'İptal', | cancel: 'İptal', | ||||
| }, | }, |
| editAppTitle: 'Редагувати інформацію про додаток', | editAppTitle: 'Редагувати інформацію про додаток', | ||||
| editDone: 'Інформація про додаток оновлена', | editDone: 'Інформація про додаток оновлена', | ||||
| editFailed: 'Не вдалося оновити інформацію про додаток', | editFailed: 'Не вдалося оновити інформацію про додаток', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'OK', | ok: 'OK', | ||||
| cancel: 'Скасувати', | cancel: 'Скасувати', | ||||
| }, | }, |
| editAppTitle: 'Chỉnh sửa thông tin ứng dụng', | editAppTitle: 'Chỉnh sửa thông tin ứng dụng', | ||||
| editDone: 'Thông tin ứng dụng đã được cập nhật', | editDone: 'Thông tin ứng dụng đã được cập nhật', | ||||
| editFailed: 'Không thể cập nhật thông tin ứng dụng', | editFailed: 'Không thể cập nhật thông tin ứng dụng', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: 'Đồng ý', | ok: 'Đồng ý', | ||||
| cancel: 'Hủy', | cancel: 'Hủy', | ||||
| }, | }, |
| editAppTitle: '编辑应用信息', | editAppTitle: '编辑应用信息', | ||||
| editDone: '应用信息已更新', | editDone: '应用信息已更新', | ||||
| editFailed: '更新应用信息失败', | editFailed: '更新应用信息失败', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: '确认', | ok: '确认', | ||||
| cancel: '取消', | cancel: '取消', | ||||
| emoji: '表情符号', | |||||
| image: '图片', | |||||
| }, | }, | ||||
| switch: '迁移为工作流编排', | switch: '迁移为工作流编排', | ||||
| switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将', | switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将', |
| editAppTitle: '編輯應用資訊', | editAppTitle: '編輯應用資訊', | ||||
| editDone: '應用資訊已更新', | editDone: '應用資訊已更新', | ||||
| editFailed: '更新應用資訊失敗', | editFailed: '更新應用資訊失敗', | ||||
| emoji: { | |||||
| iconPicker: { | |||||
| ok: '確認', | ok: '確認', | ||||
| cancel: '取消', | cancel: '取消', | ||||
| }, | }, |
| import type { DataSourceNotionPage } from './common' | import type { DataSourceNotionPage } from './common' | ||||
| import type { AppMode, RetrievalConfig } from '@/types/app' | |||||
| import type { AppIconType, AppMode, RetrievalConfig } from '@/types/app' | |||||
| import type { Tag } from '@/app/components/base/tag-management/constant' | import type { Tag } from '@/app/components/base/tag-management/constant' | ||||
| export enum DataSourceType { | export enum DataSourceType { | ||||
| id: string | id: string | ||||
| name: string | name: string | ||||
| mode: AppMode | mode: AppMode | ||||
| icon_type: AppIconType | null | |||||
| icon: string | icon: string | ||||
| icon_background: string | icon_background: string | ||||
| icon_url: string | |||||
| } | } | ||||
| export type RelatedAppResponse = { | export type RelatedAppResponse = { |
| import type { AppMode } from '@/types/app' | |||||
| import type { AppIconType, AppMode } from '@/types/app' | |||||
| export type AppBasicInfo = { | export type AppBasicInfo = { | ||||
| id: string | id: string | ||||
| mode: AppMode | mode: AppMode | ||||
| icon_type: AppIconType | null | |||||
| icon: string | icon: string | ||||
| icon_background: string | icon_background: string | ||||
| icon_url: string | |||||
| name: string | name: string | ||||
| description: string | description: string | ||||
| } | } |
| import type { Locale } from '@/i18n' | import type { Locale } from '@/i18n' | ||||
| import type { AppIconType } from '@/types/app' | |||||
| export type ResponseHolder = {} | export type ResponseHolder = {} | ||||
| title: string | title: string | ||||
| chat_color_theme?: string | chat_color_theme?: string | ||||
| chat_color_theme_inverted?: boolean | chat_color_theme_inverted?: boolean | ||||
| icon_type?: AppIconType | |||||
| icon?: string | icon?: string | ||||
| icon_background?: string | icon_background?: string | ||||
| icon_url?: string | |||||
| description?: string | description?: string | ||||
| default_language?: Locale | default_language?: Locale | ||||
| prompt_public?: boolean | prompt_public?: boolean |
| "react": "~18.2.0", | "react": "~18.2.0", | ||||
| "react-18-input-autosize": "^3.0.0", | "react-18-input-autosize": "^3.0.0", | ||||
| "react-dom": "~18.2.0", | "react-dom": "~18.2.0", | ||||
| "react-easy-crop": "^5.0.8", | |||||
| "react-error-boundary": "^4.0.2", | "react-error-boundary": "^4.0.2", | ||||
| "react-headless-pagination": "^1.1.4", | "react-headless-pagination": "^1.1.4", | ||||
| "react-hook-form": "^7.51.4", | "react-hook-form": "^7.51.4", |
| import { del, get, patch, post, put } from './base' | import { del, get, patch, post, put } from './base' | ||||
| import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' | import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' | ||||
| import type { CommonResponse } from '@/models/common' | import type { CommonResponse } from '@/models/common' | ||||
| import type { AppMode, ModelConfig } from '@/types/app' | |||||
| import type { AppIconType, AppMode, ModelConfig } from '@/types/app' | |||||
| import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' | import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' | ||||
| export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => { | export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => { | ||||
| return get<AppTemplatesResponse>(url) | return get<AppTemplatesResponse>(url) | ||||
| } | } | ||||
| export const createApp: Fetcher<AppDetailResponse, { name: string; icon: string; icon_background: string; mode: AppMode; description?: string; config?: ModelConfig }> = ({ name, icon, icon_background, mode, description, config }) => { | |||||
| return post<AppDetailResponse>('apps', { body: { name, icon, icon_background, mode, description, model_config: config } }) | |||||
| export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: AppIconType; icon?: string; icon_background?: string; mode: AppMode; description?: string; config?: ModelConfig }> = ({ name, icon_type, icon, icon_background, mode, description, config }) => { | |||||
| return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } }) | |||||
| } | } | ||||
| export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon: string; icon_background: string; description: string }> = ({ appID, name, icon, icon_background, description }) => { | |||||
| return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon, icon_background, description } }) | |||||
| export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string }> = ({ appID, name, icon_type, icon, icon_background, description }) => { | |||||
| return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description } }) | |||||
| } | } | ||||
| export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon: string; icon_background: string; mode: AppMode; description?: string }> = ({ appID, name, icon, icon_background, mode, description }) => { | |||||
| return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon, icon_background, mode, description } }) | |||||
| export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppMode; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => { | |||||
| return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } }) | |||||
| } | } | ||||
| export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include?: boolean }> = ({ appID, include = false }) => { | export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include?: boolean }> = ({ appID, include = false }) => { | ||||
| return get<{ data: string }>(`apps/${appID}/export?include_secret=${include}`) | return get<{ data: string }>(`apps/${appID}/export?include_secret=${include}`) | ||||
| } | } | ||||
| export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ data, name, description, icon, icon_background }) => { | |||||
| return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon, icon_background } }) | |||||
| export const importApp: Fetcher<AppDetailResponse, { data: string; name?: string; description?: string; icon_type?: AppIconType; icon?: string; icon_background?: string }> = ({ data, name, description, icon_type, icon, icon_background }) => { | |||||
| return post<AppDetailResponse>('apps/import', { body: { data, name, description, icon_type, icon, icon_background } }) | |||||
| } | } | ||||
| export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => { | export const importAppFromUrl: Fetcher<AppDetailResponse, { url: string; name?: string; description?: string; icon?: string; icon_background?: string }> = ({ url, name, description, icon, icon_background }) => { | ||||
| return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } }) | return post<AppDetailResponse>('apps/import/url', { body: { url, name, description, icon, icon_background } }) | ||||
| } | } | ||||
| export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon: string; icon_background: string }> = ({ appID, name, icon, icon_background }) => { | |||||
| return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon, icon_background } }) | |||||
| export const switchApp: Fetcher<{ new_app_id: string }, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null }> = ({ appID, name, icon_type, icon, icon_background }) => { | |||||
| return post<{ new_app_id: string }>(`apps/${appID}/convert-to-workflow`, { body: { name, icon_type, icon, icon_background } }) | |||||
| } | } | ||||
| export const deleteApp: Fetcher<CommonResponse, string> = (appID) => { | export const deleteApp: Fetcher<CommonResponse, string> = (appID) => { |
| /** Custom Disclaimer */ | /** Custom Disclaimer */ | ||||
| custom_disclaimer: string | custom_disclaimer: string | ||||
| icon_type: AppIconType | null | |||||
| icon: string | icon: string | ||||
| icon_background: string | |||||
| icon_background: string | null | |||||
| icon_url: string | null | |||||
| show_workflow_steps: boolean | show_workflow_steps: boolean | ||||
| } | } | ||||
| export type AppIconType = 'image' | 'emoji' | |||||
| /** | /** | ||||
| * App | * App | ||||
| */ | */ | ||||
| /** Description */ | /** Description */ | ||||
| description: string | description: string | ||||
| /** Icon */ | |||||
| /** | |||||
| * Icon Type | |||||
| * @default 'emoji' | |||||
| */ | |||||
| icon_type: AppIconType | null | |||||
| /** Icon, stores file ID if icon_type is 'image' */ | |||||
| icon: string | icon: string | ||||
| /** Icon Background */ | |||||
| icon_background: string | |||||
| /** Icon Background, only available when icon_type is null or 'emoji' */ | |||||
| icon_background: string | null | |||||
| /** Icon URL, only available when icon_type is 'image' */ | |||||
| icon_url: string | null | |||||
| /** Mode */ | /** Mode */ | ||||
| mode: AppMode | mode: AppMode |
| resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" | resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" | ||||
| integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== | integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== | ||||
| normalize-wheel@^1.0.1: | |||||
| version "1.0.1" | |||||
| resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45" | |||||
| integrity sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA== | |||||
| npm-run-path@^4.0.1: | npm-run-path@^4.0.1: | ||||
| version "4.0.1" | version "4.0.1" | ||||
| resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" | resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" | ||||
| loose-envify "^1.1.0" | loose-envify "^1.1.0" | ||||
| scheduler "^0.23.0" | scheduler "^0.23.0" | ||||
| react-easy-crop@^5.0.8: | |||||
| version "5.0.8" | |||||
| resolved "https://registry.yarnpkg.com/react-easy-crop/-/react-easy-crop-5.0.8.tgz#6cf5be061c0ec6dc0c6ee7413974c34e35bf7475" | |||||
| integrity sha512-KjulxXhR5iM7+ATN2sGCum/IyDxGw7xT0dFoGcqUP+ysaPU5Ka7gnrDa2tUHFHUoMNyPrVZ05QA+uvMgC5ym/g== | |||||
| dependencies: | |||||
| normalize-wheel "^1.0.1" | |||||
| tslib "^2.0.1" | |||||
| react-error-boundary@^3.1.4: | react-error-boundary@^3.1.4: | ||||
| version "3.1.4" | version "3.1.4" | ||||
| resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz" | resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz" | ||||
| char-regex "^1.0.2" | char-regex "^1.0.2" | ||||
| strip-ansi "^6.0.0" | strip-ansi "^6.0.0" | ||||
| "string-width-cjs@npm:string-width@^4.2.0": | |||||
| version "4.2.3" | |||||
| resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" | |||||
| integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | |||||
| dependencies: | |||||
| emoji-regex "^8.0.0" | |||||
| is-fullwidth-code-point "^3.0.0" | |||||
| strip-ansi "^6.0.1" | |||||
| string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: | |||||
| "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: | |||||
| version "4.2.3" | version "4.2.3" | ||||
| resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" | resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" | ||||
| integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | ||||
| character-entities-html4 "^2.0.0" | character-entities-html4 "^2.0.0" | ||||
| character-entities-legacy "^3.0.0" | character-entities-legacy "^3.0.0" | ||||
| "strip-ansi-cjs@npm:strip-ansi@^6.0.1": | |||||
| version "6.0.1" | |||||
| resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" | |||||
| integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | |||||
| dependencies: | |||||
| ansi-regex "^5.0.1" | |||||
| strip-ansi@^6.0.0, strip-ansi@^6.0.1: | |||||
| "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: | |||||
| version "6.0.1" | version "6.0.1" | ||||
| resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" | resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" | ||||
| integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | ||||
| resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" | resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" | ||||
| integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== | ||||
| tslib@^2.0.1: | |||||
| version "2.6.3" | |||||
| resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" | |||||
| integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== | |||||
| tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0: | tslib@^2.1.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0: | ||||
| version "2.5.3" | version "2.5.3" | ||||
| resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz" | resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz" | ||||
| resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" | resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" | ||||
| integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== | integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== | ||||
| "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": | |||||
| "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: | |||||
| version "7.0.0" | version "7.0.0" | ||||
| resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" | resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" | ||||
| integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== | ||||
| string-width "^4.1.0" | string-width "^4.1.0" | ||||
| strip-ansi "^6.0.0" | strip-ansi "^6.0.0" | ||||
| wrap-ansi@^7.0.0: | |||||
| version "7.0.0" | |||||
| resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" | |||||
| integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== | |||||
| dependencies: | |||||
| ansi-styles "^4.0.0" | |||||
| string-width "^4.1.0" | |||||
| strip-ansi "^6.0.0" | |||||
| wrap-ansi@^8.1.0: | wrap-ansi@^8.1.0: | ||||
| version "8.1.0" | version "8.1.0" | ||||
| resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" | resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" |