Co-authored-by: crazywoola <427733928@qq.com>tags/0.7.1
| @@ -61,6 +61,7 @@ class AppListApi(Resource): | |||
| parser.add_argument('name', type=str, required=True, 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('icon_type', type=str, location='json') | |||
| parser.add_argument('icon', type=str, location='json') | |||
| parser.add_argument('icon_background', type=str, location='json') | |||
| args = parser.parse_args() | |||
| @@ -94,6 +95,7 @@ class AppImportApi(Resource): | |||
| parser.add_argument('data', type=str, required=True, nullable=False, location='json') | |||
| parser.add_argument('name', 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_background', type=str, location='json') | |||
| args = parser.parse_args() | |||
| @@ -167,6 +169,7 @@ class AppApi(Resource): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument('name', type=str, required=True, nullable=False, 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_background', type=str, location='json') | |||
| parser.add_argument('max_active_requests', type=int, location='json') | |||
| @@ -208,6 +211,7 @@ class AppCopyApi(Resource): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument('name', 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_background', type=str, location='json') | |||
| args = parser.parse_args() | |||
| @@ -16,6 +16,7 @@ from models.model import Site | |||
| def parse_app_site_args(): | |||
| parser = reqparse.RequestParser() | |||
| 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_background', type=str, required=False, location='json') | |||
| parser.add_argument('description', type=str, required=False, location='json') | |||
| @@ -53,6 +54,7 @@ class AppSite(Resource): | |||
| for attr_name in [ | |||
| 'title', | |||
| 'icon_type', | |||
| 'icon', | |||
| 'icon_background', | |||
| 'description', | |||
| @@ -459,6 +459,7 @@ class ConvertToWorkflowApi(Resource): | |||
| if request.data: | |||
| parser = reqparse.RequestParser() | |||
| 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_background', type=str, required=False, nullable=True, location='json') | |||
| args = parser.parse_args() | |||
| @@ -6,6 +6,7 @@ from configs import dify_config | |||
| from controllers.web import api | |||
| from controllers.web.wraps import WebApiResource | |||
| from extensions.ext_database import db | |||
| from libs.helper import AppIconUrlField | |||
| from models.account import TenantStatus | |||
| from models.model import Site | |||
| from services.feature_service import FeatureService | |||
| @@ -28,8 +29,10 @@ class AppSiteApi(WebApiResource): | |||
| 'title': fields.String, | |||
| 'chat_color_theme': fields.String, | |||
| 'chat_color_theme_inverted': fields.Boolean, | |||
| 'icon_type': fields.String, | |||
| 'icon': fields.String, | |||
| 'icon_background': fields.String, | |||
| 'icon_url': AppIconUrlField, | |||
| 'description': fields.String, | |||
| 'copyright': fields.String, | |||
| 'privacy_policy': fields.String, | |||
| @@ -11,6 +11,7 @@ def handle(sender, **kwargs): | |||
| site = Site( | |||
| app_id=app.id, | |||
| title=app.name, | |||
| icon_type=app.icon_type, | |||
| icon=app.icon, | |||
| icon_background=app.icon_background, | |||
| default_language=account.interface_language, | |||
| @@ -1,14 +1,16 @@ | |||
| from flask_restful import fields | |||
| from libs.helper import TimestampField | |||
| from libs.helper import AppIconUrlField, TimestampField | |||
| app_detail_kernel_fields = { | |||
| "id": fields.String, | |||
| "name": fields.String, | |||
| "description": fields.String, | |||
| "mode": fields.String(attribute="mode_compatible_with_agent"), | |||
| "icon_type": fields.String, | |||
| "icon": fields.String, | |||
| "icon_background": fields.String, | |||
| "icon_url": AppIconUrlField, | |||
| } | |||
| related_app_list = { | |||
| @@ -71,8 +73,10 @@ app_partial_fields = { | |||
| "max_active_requests": fields.Raw(), | |||
| "description": fields.String(attribute="desc_or_prompt"), | |||
| "mode": fields.String(attribute="mode_compatible_with_agent"), | |||
| "icon_type": fields.String, | |||
| "icon": fields.String, | |||
| "icon_background": fields.String, | |||
| "icon_url": AppIconUrlField, | |||
| "model_config": fields.Nested(model_config_partial_fields, attribute="app_model_config", allow_null=True), | |||
| "created_at": TimestampField, | |||
| "tags": fields.List(fields.Nested(tag_fields)), | |||
| @@ -104,8 +108,10 @@ site_fields = { | |||
| "access_token": fields.String(attribute="code"), | |||
| "code": fields.String, | |||
| "title": fields.String, | |||
| "icon_type": fields.String, | |||
| "icon": fields.String, | |||
| "icon_background": fields.String, | |||
| "icon_url": AppIconUrlField, | |||
| "description": fields.String, | |||
| "default_language": fields.String, | |||
| "chat_color_theme": fields.String, | |||
| @@ -125,8 +131,10 @@ app_detail_fields_with_site = { | |||
| "name": fields.String, | |||
| "description": fields.String, | |||
| "mode": fields.String(attribute="mode_compatible_with_agent"), | |||
| "icon_type": fields.String, | |||
| "icon": fields.String, | |||
| "icon_background": fields.String, | |||
| "icon_url": AppIconUrlField, | |||
| "enable_site": fields.Boolean, | |||
| "enable_api": fields.Boolean, | |||
| "model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True), | |||
| @@ -1,13 +1,15 @@ | |||
| from flask_restful import fields | |||
| from libs.helper import TimestampField | |||
| from libs.helper import AppIconUrlField, TimestampField | |||
| app_fields = { | |||
| "id": fields.String, | |||
| "name": fields.String, | |||
| "mode": fields.String, | |||
| "icon_type": fields.String, | |||
| "icon": fields.String, | |||
| "icon_background": fields.String, | |||
| "icon_url": AppIconUrlField, | |||
| } | |||
| installed_app_fields = { | |||
| @@ -16,6 +16,7 @@ from flask import Response, current_app, stream_with_context | |||
| from flask_restful import fields | |||
| 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 models.account import Account | |||
| @@ -24,6 +25,18 @@ def run(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): | |||
| def format(self, value) -> int: | |||
| return int(value.timestamp()) | |||
| @@ -0,0 +1,39 @@ | |||
| """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 ### | |||
| @@ -51,6 +51,10 @@ class AppMode(Enum): | |||
| raise ValueError(f'invalid mode value {value}') | |||
| class IconType(Enum): | |||
| IMAGE = "image" | |||
| EMOJI = "emoji" | |||
| class App(db.Model): | |||
| __tablename__ = 'apps' | |||
| __table_args__ = ( | |||
| @@ -63,6 +67,7 @@ class App(db.Model): | |||
| name = db.Column(db.String(255), nullable=False) | |||
| description = db.Column(db.Text, nullable=False, server_default=db.text("''::character varying")) | |||
| mode = db.Column(db.String(255), nullable=False) | |||
| icon_type = db.Column(db.String(255), nullable=True) | |||
| icon = db.Column(db.String(255)) | |||
| icon_background = db.Column(db.String(255)) | |||
| app_model_config_id = db.Column(StringUUID, nullable=True) | |||
| @@ -1087,6 +1092,7 @@ class Site(db.Model): | |||
| id = db.Column(StringUUID, server_default=db.text('uuid_generate_v4()')) | |||
| app_id = db.Column(StringUUID, 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_background = db.Column(db.String(255)) | |||
| description = db.Column(db.Text) | |||
| @@ -82,6 +82,7 @@ class AppDslService: | |||
| # get app basic info | |||
| 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', '') | |||
| 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_background = args.get("icon_background") if args.get("icon_background") \ | |||
| else app_data.get('icon_background') | |||
| @@ -96,6 +97,7 @@ class AppDslService: | |||
| account=account, | |||
| name=name, | |||
| description=description, | |||
| icon_type=icon_type, | |||
| icon=icon, | |||
| icon_background=icon_background | |||
| ) | |||
| @@ -107,6 +109,7 @@ class AppDslService: | |||
| account=account, | |||
| name=name, | |||
| description=description, | |||
| icon_type=icon_type, | |||
| icon=icon, | |||
| icon_background=icon_background | |||
| ) | |||
| @@ -165,8 +168,8 @@ class AppDslService: | |||
| "app": { | |||
| "name": app_model.name, | |||
| "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 | |||
| } | |||
| } | |||
| @@ -207,6 +210,7 @@ class AppDslService: | |||
| account: Account, | |||
| name: str, | |||
| description: str, | |||
| icon_type: str, | |||
| icon: str, | |||
| icon_background: str) -> App: | |||
| """ | |||
| @@ -218,6 +222,7 @@ class AppDslService: | |||
| :param account: Account instance | |||
| :param name: app name | |||
| :param description: app description | |||
| :param icon_type: app icon type, "emoji" or "image" | |||
| :param icon: app icon | |||
| :param icon_background: app icon background | |||
| """ | |||
| @@ -231,6 +236,7 @@ class AppDslService: | |||
| account=account, | |||
| name=name, | |||
| description=description, | |||
| icon_type=icon_type, | |||
| icon=icon, | |||
| icon_background=icon_background | |||
| ) | |||
| @@ -307,6 +313,7 @@ class AppDslService: | |||
| account: Account, | |||
| name: str, | |||
| description: str, | |||
| icon_type: str, | |||
| icon: str, | |||
| icon_background: str) -> App: | |||
| """ | |||
| @@ -331,6 +338,7 @@ class AppDslService: | |||
| account=account, | |||
| name=name, | |||
| description=description, | |||
| icon_type=icon_type, | |||
| icon=icon, | |||
| icon_background=icon_background | |||
| ) | |||
| @@ -358,6 +366,7 @@ class AppDslService: | |||
| account: Account, | |||
| name: str, | |||
| description: str, | |||
| icon_type: str, | |||
| icon: str, | |||
| icon_background: str) -> App: | |||
| """ | |||
| @@ -368,6 +377,7 @@ class AppDslService: | |||
| :param account: Account instance | |||
| :param name: app name | |||
| :param description: app description | |||
| :param icon_type: app icon type, "emoji" or "image" | |||
| :param icon: app icon | |||
| :param icon_background: app icon background | |||
| """ | |||
| @@ -376,6 +386,7 @@ class AppDslService: | |||
| mode=app_mode.value, | |||
| name=name, | |||
| description=description, | |||
| icon_type=icon_type, | |||
| icon=icon, | |||
| icon_background=icon_background, | |||
| enable_site=True, | |||
| @@ -119,6 +119,7 @@ class AppService: | |||
| app.name = args['name'] | |||
| app.description = args.get('description', '') | |||
| app.mode = args['mode'] | |||
| app.icon_type = args.get('icon_type', 'emoji') | |||
| app.icon = args['icon'] | |||
| app.icon_background = args['icon_background'] | |||
| app.tenant_id = tenant_id | |||
| @@ -210,6 +211,7 @@ class AppService: | |||
| app.name = args.get('name') | |||
| app.description = args.get('description', '') | |||
| app.max_active_requests = args.get('max_active_requests') | |||
| app.icon_type = args.get('icon_type', 'emoji') | |||
| app.icon = args.get('icon') | |||
| app.icon_background = args.get('icon_background') | |||
| app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) | |||
| @@ -35,6 +35,7 @@ class WorkflowConverter: | |||
| def convert_to_workflow(self, app_model: App, | |||
| account: Account, | |||
| name: str, | |||
| icon_type: str, | |||
| icon: str, | |||
| icon_background: str) -> App: | |||
| """ | |||
| @@ -50,6 +51,7 @@ class WorkflowConverter: | |||
| :param account: Account | |||
| :param name: new app name | |||
| :param icon: new app icon | |||
| :param icon_type: new app icon type | |||
| :param icon_background: new app icon background | |||
| :return: new App instance | |||
| """ | |||
| @@ -66,6 +68,7 @@ class WorkflowConverter: | |||
| new_app.name = name if name else app_model.name + '(workflow)' | |||
| new_app.mode = AppMode.ADVANCED_CHAT.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_background = icon_background if icon_background else app_model.icon_background | |||
| new_app.enable_site = app_model.enable_site | |||
| @@ -302,6 +302,7 @@ class WorkflowService: | |||
| app_model=app_model, | |||
| account=account, | |||
| name=args.get('name'), | |||
| icon_type=args.get('icon_type'), | |||
| icon=args.get('icon'), | |||
| icon_background=args.get('icon_background'), | |||
| ) | |||
| @@ -15,4 +15,7 @@ NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api | |||
| NEXT_PUBLIC_SENTRY_DSN= | |||
| # 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 | |||
| @@ -75,6 +75,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| description, | |||
| @@ -83,6 +84,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| await updateAppInfo({ | |||
| appID: app.id, | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| description, | |||
| @@ -101,11 +103,12 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| } | |||
| }, [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 { | |||
| const newApp = await copyApp({ | |||
| appID: app.id, | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| mode: app.mode, | |||
| @@ -258,8 +261,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| <div className='relative shrink-0'> | |||
| <AppIcon | |||
| size="large" | |||
| iconType={app.icon_type} | |||
| icon={app.icon} | |||
| 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'> | |||
| {app.mode === 'advanced-chat' && ( | |||
| @@ -360,9 +365,11 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| {showEditModal && ( | |||
| <EditAppModal | |||
| isEditModal | |||
| appName={app.name} | |||
| appIconType={app.icon_type} | |||
| appIcon={app.icon} | |||
| appIconBackground={app.icon_background} | |||
| appName={app.name} | |||
| appIconUrl={app.icon_url} | |||
| appDescription={app.description} | |||
| show={showEditModal} | |||
| onConfirm={onEdit} | |||
| @@ -372,8 +379,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { | |||
| {showDuplicateModal && ( | |||
| <DuplicateAppModal | |||
| appName={app.name} | |||
| icon_type={app.icon_type} | |||
| icon={app.icon} | |||
| icon_background={app.icon_background} | |||
| icon_url={app.icon_url} | |||
| show={showDuplicateModal} | |||
| onConfirm={onCopy} | |||
| onHide={() => setShowDuplicateModal(false)} | |||
| @@ -60,7 +60,7 @@ const LikedItem = ({ | |||
| return ( | |||
| <Link className={classNames(s.itemWrapper, 'px-2', isMobile && 'justify-center')} href={`/app/${detail?.id}/overview`}> | |||
| <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' && ( | |||
| <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' && ( | |||
| @@ -59,6 +59,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| description, | |||
| @@ -69,6 +70,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| const app = await updateAppInfo({ | |||
| appID: appDetail.id, | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| description, | |||
| @@ -86,13 +88,14 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| } | |||
| }, [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) | |||
| return | |||
| try { | |||
| const newApp = await copyApp({ | |||
| appID: appDetail.id, | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| mode: appDetail.mode, | |||
| @@ -194,7 +197,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| > | |||
| <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'> | |||
| <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( | |||
| '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]', | |||
| @@ -257,7 +266,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| {/* header */} | |||
| <div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-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'> | |||
| {appDetail.mode === 'advanced-chat' && ( | |||
| <ChatBot className='w-3 h-3 text-[#1570EF]' /> | |||
| @@ -402,9 +417,11 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| {showEditModal && ( | |||
| <CreateAppModal | |||
| isEditModal | |||
| appName={appDetail.name} | |||
| appIconType={appDetail.icon_type} | |||
| appIcon={appDetail.icon} | |||
| appIconBackground={appDetail.icon_background} | |||
| appName={appDetail.name} | |||
| appIconUrl={appDetail.icon_url} | |||
| appDescription={appDetail.description} | |||
| show={showEditModal} | |||
| onConfirm={onEdit} | |||
| @@ -414,8 +431,10 @@ const AppInfo = ({ expand }: IAppInfoProps) => { | |||
| {showDuplicateModal && ( | |||
| <DuplicateAppModal | |||
| appName={appDetail.name} | |||
| icon_type={appDetail.icon_type} | |||
| icon={appDetail.icon} | |||
| icon_background={appDetail.icon_background} | |||
| icon_url={appDetail.icon_url} | |||
| show={showDuplicateModal} | |||
| onConfirm={onCopy} | |||
| onHide={() => setShowDuplicateModal(false)} | |||
| @@ -8,6 +8,8 @@ import { | |||
| } from '@remixicon/react' | |||
| import { useRouter } from 'next/navigation' | |||
| 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 cn from '@/utils/classnames' | |||
| import AppsContext, { useAppContext } from '@/context/app-context' | |||
| @@ -18,7 +20,6 @@ import { createApp } from '@/service/apps' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Button from '@/app/components/base/button' | |||
| 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 { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication' | |||
| import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel' | |||
| @@ -40,8 +41,8 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { | |||
| const [appMode, setAppMode] = useState<AppMode>('chat') | |||
| 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 [description, setDescription] = useState('') | |||
| @@ -66,8 +67,9 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { | |||
| const app = await createApp({ | |||
| name, | |||
| 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, | |||
| }) | |||
| notify({ type: 'success', message: t('app.newApp.appCreated') }) | |||
| @@ -81,7 +83,7 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { | |||
| notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) | |||
| } | |||
| 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 ( | |||
| <Modal | |||
| @@ -269,7 +271,14 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { | |||
| <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='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 | |||
| value={name} | |||
| onChange={e => setName(e.target.value)} | |||
| @@ -277,14 +286,13 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { | |||
| 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> | |||
| {showEmojiPicker && <EmojiPicker | |||
| onSelect={(icon, icon_background) => { | |||
| setEmoji({ icon, icon_background }) | |||
| setShowEmojiPicker(false) | |||
| {showAppIconPicker && <AppIconPicker | |||
| onSelect={(payload) => { | |||
| setAppIcon(payload) | |||
| setShowAppIconPicker(false) | |||
| }} | |||
| onClose={() => { | |||
| setEmoji({ icon: '🤖', icon_background: '#FFEAD5' }) | |||
| setShowEmojiPicker(false) | |||
| setShowAppIconPicker(false) | |||
| }} | |||
| />} | |||
| </div> | |||
| @@ -1,32 +1,39 @@ | |||
| 'use client' | |||
| import React, { useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import AppIconPicker from '../../base/app-icon-picker' | |||
| import s from './style.module.css' | |||
| import cn from '@/utils/classnames' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Button from '@/app/components/base/button' | |||
| import Toast from '@/app/components/base/toast' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import EmojiPicker from '@/app/components/base/emoji-picker' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| import AppsFull from '@/app/components/billing/apps-full-in-dialog' | |||
| import type { AppIconType } from '@/types/app' | |||
| export type DuplicateAppModalProps = { | |||
| appName: string | |||
| icon_type: AppIconType | null | |||
| icon: string | |||
| icon_background: string | |||
| icon_background?: string | null | |||
| icon_url?: string | null | |||
| show: boolean | |||
| onConfirm: (info: { | |||
| name: string | |||
| icon_type: AppIconType | |||
| icon: string | |||
| icon_background: string | |||
| icon_background?: string | null | |||
| }) => Promise<void> | |||
| onHide: () => void | |||
| } | |||
| const DuplicateAppModal = ({ | |||
| appName, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| icon_url, | |||
| show = false, | |||
| onConfirm, | |||
| onHide, | |||
| @@ -35,8 +42,12 @@ const DuplicateAppModal = ({ | |||
| 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 isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) | |||
| @@ -48,7 +59,9 @@ const DuplicateAppModal = ({ | |||
| } | |||
| onConfirm({ | |||
| name, | |||
| ...emoji, | |||
| icon_type: appIcon.type, | |||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||
| icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, | |||
| }) | |||
| onHide() | |||
| } | |||
| @@ -65,7 +78,15 @@ const DuplicateAppModal = ({ | |||
| <div className={s.content}> | |||
| <div className={s.subTitle}>{t('explore.appCustomize.subTitle')}</div> | |||
| <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 | |||
| value={name} | |||
| onChange={e => setName(e.target.value)} | |||
| @@ -79,14 +100,16 @@ const DuplicateAppModal = ({ | |||
| <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button> | |||
| </div> | |||
| </Modal> | |||
| {showEmojiPicker && <EmojiPicker | |||
| onSelect={(icon, icon_background) => { | |||
| setEmoji({ icon, icon_background }) | |||
| setShowEmojiPicker(false) | |||
| {showAppIconPicker && <AppIconPicker | |||
| onSelect={(payload) => { | |||
| setAppIcon(payload) | |||
| setShowAppIconPicker(false) | |||
| }} | |||
| 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) | |||
| }} | |||
| />} | |||
| </> | |||
| @@ -10,11 +10,11 @@ import Button from '@/app/components/base/button' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import { SimpleSelect } from '@/app/components/base/select' | |||
| 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 { 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 = { | |||
| isChat: boolean | |||
| @@ -35,8 +35,9 @@ export type ConfigParams = { | |||
| copyright: string | |||
| privacy_policy: string | |||
| custom_disclaimer: string | |||
| icon_type: AppIconType | |||
| icon: string | |||
| icon_background: string | |||
| icon_background?: string | |||
| show_workflow_steps: boolean | |||
| } | |||
| @@ -51,9 +52,12 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| }) => { | |||
| const { notify } = useToastContext() | |||
| const [isShowMore, setIsShowMore] = useState(false) | |||
| const { icon, icon_background } = appInfo | |||
| const { | |||
| title, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| icon_url, | |||
| description, | |||
| chat_color_theme, | |||
| chat_color_theme_inverted, | |||
| @@ -76,9 +80,13 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| const [language, setLanguage] = useState(default_language) | |||
| const [saveLoading, setSaveLoading] = useState(false) | |||
| 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(() => { | |||
| setInputInfo({ | |||
| @@ -92,7 +100,9 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| show_workflow_steps, | |||
| }) | |||
| 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]) | |||
| const onHide = () => { | |||
| @@ -135,8 +145,9 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| copyright: inputInfo.copyright, | |||
| privacy_policy: inputInfo.privacyPolicy, | |||
| 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, | |||
| } | |||
| await onSave?.(params) | |||
| @@ -167,10 +178,12 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div> | |||
| <div className='flex mt-2'> | |||
| <AppIcon size='large' | |||
| onClick={() => { setShowEmojiPicker(true) }} | |||
| onClick={() => { setShowAppIconPicker(true) }} | |||
| 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`} | |||
| value={inputInfo.title} | |||
| @@ -250,14 +263,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({ | |||
| <Button className='mr-2' onClick={onHide}>{t('common.operation.cancel')}</Button> | |||
| <Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button> | |||
| </div> | |||
| {showEmojiPicker && <EmojiPicker | |||
| onSelect={(icon, icon_background) => { | |||
| setEmoji({ icon, icon_background }) | |||
| setShowEmojiPicker(false) | |||
| {showAppIconPicker && <AppIconPicker | |||
| onSelect={(payload) => { | |||
| setAppIcon(payload) | |||
| setShowAppIconPicker(false) | |||
| }} | |||
| 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 > | |||
| @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation' | |||
| import { useContext } from 'use-context-selector' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiCloseLine } from '@remixicon/react' | |||
| import AppIconPicker from '../../base/app-icon-picker' | |||
| import s from './style.module.css' | |||
| import cn from '@/utils/classnames' | |||
| import Button from '@/app/components/base/button' | |||
| @@ -15,7 +16,6 @@ import { deleteApp, switchApp } from '@/service/apps' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| 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 { getRedirection } from '@/utils/app-redirection' | |||
| import type { App } from '@/types/app' | |||
| @@ -41,8 +41,13 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo | |||
| const { plan, enableBilling } = useProviderContext() | |||
| 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 [removeOriginal, setRemoveOriginal] = useState<boolean>(false) | |||
| const [showConfirmDelete, setShowConfirmDelete] = useState(false) | |||
| @@ -52,8 +57,9 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo | |||
| const { new_app_id: newAppID } = await switchApp({ | |||
| appID: appDetail.id, | |||
| 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) | |||
| onSuccess() | |||
| @@ -106,7 +112,15 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo | |||
| <div className='pb-4'> | |||
| <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'> | |||
| <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 | |||
| value={name} | |||
| onChange={e => setName(e.target.value)} | |||
| @@ -114,14 +128,16 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo | |||
| 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> | |||
| {showEmojiPicker && <EmojiPicker | |||
| onSelect={(icon, icon_background) => { | |||
| setEmoji({ icon, icon_background }) | |||
| setShowEmojiPicker(false) | |||
| {showAppIconPicker && <AppIconPicker | |||
| onSelect={(payload) => { | |||
| setAppIcon(payload) | |||
| setShowAppIconPicker(false) | |||
| }} | |||
| 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> | |||
| @@ -0,0 +1,97 @@ | |||
| '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 | |||
| @@ -0,0 +1,43 @@ | |||
| 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, | |||
| } | |||
| } | |||
| @@ -0,0 +1,139 @@ | |||
| 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 | |||
| @@ -0,0 +1,12 @@ | |||
| .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; | |||
| } | |||
| @@ -0,0 +1,98 @@ | |||
| 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') | |||
| }) | |||
| } | |||
| @@ -1,17 +1,21 @@ | |||
| import type { FC } from 'react' | |||
| 'use client' | |||
| import data from '@emoji-mart/data' | |||
| import type { FC } from 'react' | |||
| import { init } from 'emoji-mart' | |||
| import data from '@emoji-mart/data' | |||
| import style from './style.module.css' | |||
| import classNames from '@/utils/classnames' | |||
| import type { AppIconType } from '@/types/app' | |||
| init({ data }) | |||
| export type AppIconProps = { | |||
| size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' | |||
| rounded?: boolean | |||
| iconType?: AppIconType | null | |||
| icon?: string | |||
| background?: string | |||
| background?: string | null | |||
| imageUrl?: string | null | |||
| className?: string | |||
| innerIcon?: React.ReactNode | |||
| onClick?: () => void | |||
| @@ -20,28 +24,34 @@ export type AppIconProps = { | |||
| const AppIcon: FC<AppIconProps> = ({ | |||
| size = 'medium', | |||
| rounded = false, | |||
| iconType, | |||
| icon, | |||
| background, | |||
| imageUrl, | |||
| className, | |||
| innerIcon, | |||
| 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 | |||
| @@ -1,5 +1,5 @@ | |||
| .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 { | |||
| @@ -43,7 +43,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | |||
| const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) | |||
| 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(() => { | |||
| if (isInstalledApp) { | |||
| @@ -52,8 +58,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | |||
| app_id: id, | |||
| site: { | |||
| title: app.name, | |||
| icon_type: app.icon_type, | |||
| icon: app.icon, | |||
| icon_background: app.icon_background, | |||
| icon_url: app.icon_url, | |||
| prompt_public: false, | |||
| copyright: '', | |||
| show_workflow_steps: true, | |||
| @@ -67,8 +67,10 @@ const Sidebar = () => { | |||
| <AppIcon | |||
| className='mr-3' | |||
| size='small' | |||
| iconType={appData?.site.icon_type} | |||
| icon={appData?.site.icon} | |||
| background={appData?.site.icon_background} | |||
| imageUrl={appData?.site.icon_url} | |||
| /> | |||
| <div className='py-1 text-base font-semibold text-gray-800'> | |||
| {appData?.site.title} | |||
| @@ -0,0 +1,171 @@ | |||
| '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 | |||
| @@ -1,56 +1,13 @@ | |||
| /* eslint-disable multiline-ternary */ | |||
| '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 s from './style.module.css' | |||
| import EmojiPickerInner from './Inner' | |||
| import cn from '@/utils/classnames' | |||
| import Divider from '@/app/components/base/divider' | |||
| import Button from '@/app/components/base/button' | |||
| 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 = { | |||
| isModal?: boolean | |||
| @@ -66,136 +23,43 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({ | |||
| className, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { categories } = data as EmojiMartData | |||
| 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 | |||
| @@ -26,7 +26,13 @@ const AppCard = ({ | |||
| <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='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'> | |||
| {appBasicInfo.mode === 'advanced-chat' && ( | |||
| <ChatBot className='w-3 h-3 text-[#1570EF]' /> | |||
| @@ -118,6 +118,7 @@ const Apps = ({ | |||
| const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) | |||
| const onCreate: CreateAppModalProps['onConfirm'] = async ({ | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| description, | |||
| @@ -129,6 +130,7 @@ const Apps = ({ | |||
| const app = await importApp({ | |||
| data: export_data, | |||
| name, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| description, | |||
| @@ -215,8 +217,10 @@ const Apps = ({ | |||
| </div> | |||
| {isShowCreateModal && ( | |||
| <CreateAppModal | |||
| appIconType={currApp?.app.icon_type || 'emoji'} | |||
| appIcon={currApp?.app.icon || ''} | |||
| appIconBackground={currApp?.app.icon_background || ''} | |||
| appIconUrl={currApp?.app.icon_url} | |||
| appName={currApp?.app.name || ''} | |||
| appDescription={currApp?.app.description || ''} | |||
| show={isShowCreateModal} | |||
| @@ -2,25 +2,29 @@ | |||
| import React, { useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { RiCloseLine } from '@remixicon/react' | |||
| import AppIconPicker from '../../base/app-icon-picker' | |||
| import Modal from '@/app/components/base/modal' | |||
| import Button from '@/app/components/base/button' | |||
| import Toast from '@/app/components/base/toast' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import EmojiPicker from '@/app/components/base/emoji-picker' | |||
| import { useProviderContext } from '@/context/provider-context' | |||
| import AppsFull from '@/app/components/billing/apps-full-in-dialog' | |||
| import type { AppIconType } from '@/types/app' | |||
| export type CreateAppModalProps = { | |||
| show: boolean | |||
| isEditModal?: boolean | |||
| appName: string | |||
| appDescription: string | |||
| appIconType: AppIconType | null | |||
| appIcon: string | |||
| appIconBackground: string | |||
| appIconBackground?: string | null | |||
| appIconUrl?: string | null | |||
| onConfirm: (info: { | |||
| name: string | |||
| icon_type: AppIconType | |||
| icon: string | |||
| icon_background: string | |||
| icon_background?: string | |||
| description: string | |||
| }) => Promise<void> | |||
| onHide: () => void | |||
| @@ -29,8 +33,10 @@ export type CreateAppModalProps = { | |||
| const CreateAppModal = ({ | |||
| show = false, | |||
| isEditModal = false, | |||
| appIcon, | |||
| appIconType, | |||
| appIcon: _appIcon, | |||
| appIconBackground, | |||
| appIconUrl, | |||
| appName, | |||
| appDescription, | |||
| onConfirm, | |||
| @@ -39,8 +45,12 @@ const CreateAppModal = ({ | |||
| const { t } = useTranslation() | |||
| 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 { plan, enableBilling } = useProviderContext() | |||
| @@ -53,7 +63,9 @@ const CreateAppModal = ({ | |||
| } | |||
| onConfirm({ | |||
| name, | |||
| ...emoji, | |||
| icon_type: appIcon.type, | |||
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, | |||
| icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined, | |||
| description, | |||
| }) | |||
| onHide() | |||
| @@ -80,7 +92,15 @@ const CreateAppModal = ({ | |||
| <div className='pt-2'> | |||
| <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'> | |||
| <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 | |||
| value={name} | |||
| onChange={e => setName(e.target.value)} | |||
| @@ -106,18 +126,19 @@ const CreateAppModal = ({ | |||
| <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button> | |||
| </div> | |||
| </Modal> | |||
| {showEmojiPicker && <EmojiPicker | |||
| onSelect={(icon, icon_background) => { | |||
| setEmoji({ icon, icon_background }) | |||
| setShowEmojiPicker(false) | |||
| {showAppIconPicker && <AppIconPicker | |||
| onSelect={(payload) => { | |||
| setAppIcon(payload) | |||
| setShowAppIconPicker(false) | |||
| }} | |||
| 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) | |||
| }} | |||
| />} | |||
| </> | |||
| ) | |||
| } | |||
| @@ -7,13 +7,16 @@ import s from './style.module.css' | |||
| import cn from '@/utils/classnames' | |||
| import ItemOperation from '@/app/components/explore/item-operation' | |||
| import AppIcon from '@/app/components/base/app-icon' | |||
| import type { AppIconType } from '@/types/app' | |||
| export type IAppNavItemProps = { | |||
| isMobile: boolean | |||
| name: string | |||
| id: string | |||
| icon_type: AppIconType | null | |||
| icon: string | |||
| icon_background: string | |||
| icon_url: string | |||
| isSelected: boolean | |||
| isPinned: boolean | |||
| togglePin: () => void | |||
| @@ -25,8 +28,10 @@ export default function AppNavItem({ | |||
| isMobile, | |||
| name, | |||
| id, | |||
| icon_type, | |||
| icon, | |||
| icon_background, | |||
| icon_url, | |||
| isSelected, | |||
| isPinned, | |||
| togglePin, | |||
| @@ -50,11 +55,11 @@ export default function AppNavItem({ | |||
| 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 && ( | |||
| <> | |||
| <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> | |||
| <div className='shrink-0 h-6' onClick={e => e.stopPropagation()}> | |||
| @@ -109,14 +109,16 @@ const SideBar: FC<IExploreSideBarProps> = ({ | |||
| 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 ( | |||
| <Item | |||
| key={id} | |||
| isMobile={isMobile} | |||
| name={name} | |||
| icon_type={icon_type} | |||
| icon={icon} | |||
| icon_background={icon_background} | |||
| icon_url={icon_url} | |||
| id={id} | |||
| isSelected={lastSegment?.toLowerCase() === id} | |||
| isPinned={is_pinned} | |||
| @@ -411,7 +411,13 @@ const TextGeneration: FC<IMainProps> = ({ | |||
| } | |||
| }, [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 showResSidebar = () => { | |||
| @@ -247,3 +247,5 @@ Thought: {{agent_scratchpad}} | |||
| 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 DISABLE_UPLOAD_IMAGE_AS_ICON = process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true' | |||
| @@ -1,19 +1,40 @@ | |||
| import { useAsyncEffect } from 'ahooks' | |||
| import { appDefaultIconBackground } from '@/config' | |||
| 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 () => { | |||
| if (!enable) | |||
| return | |||
| const isValidImageIcon = icon_type === 'image' && icon_url | |||
| 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>' | |||
| link.rel = 'shortcut icon' | |||
| @@ -46,7 +46,7 @@ const translation = { | |||
| editAppTitle: 'App-Informationen bearbeiten', | |||
| editDone: 'App-Informationen wurden aktualisiert', | |||
| editFailed: 'Aktualisierung der App-Informationen fehlgeschlagen', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Abbrechen', | |||
| }, | |||
| @@ -71,9 +71,11 @@ const translation = { | |||
| editAppTitle: 'Edit App Info', | |||
| editDone: 'App info updated', | |||
| editFailed: 'Failed to update app info', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Cancel', | |||
| emoji: 'Emoji', | |||
| image: 'Image', | |||
| }, | |||
| 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 ', | |||
| @@ -67,7 +67,7 @@ const translation = { | |||
| editAppTitle: 'Editar información de la app', | |||
| editDone: 'Información de la app actualizada', | |||
| editFailed: 'Error al actualizar información de la app', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Cancelar', | |||
| }, | |||
| @@ -71,7 +71,7 @@ const translation = { | |||
| editAppTitle: 'ویرایش اطلاعات برنامه', | |||
| editDone: 'اطلاعات برنامه بهروزرسانی شد', | |||
| editFailed: 'بهروزرسانی اطلاعات برنامه ناموفق بود', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'باشه', | |||
| cancel: 'لغو', | |||
| }, | |||
| @@ -67,7 +67,7 @@ const translation = { | |||
| editAppTitle: 'Modifier les informations de l\'application', | |||
| editDone: 'Informations sur l\'application mises à jour', | |||
| editFailed: 'Échec de la mise à jour des informations de l\'application', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Annuler', | |||
| }, | |||
| @@ -67,7 +67,7 @@ const translation = { | |||
| editAppTitle: 'ऐप जानकारी संपादित करें', | |||
| editDone: 'ऐप जानकारी अपडेट की गई', | |||
| editFailed: 'ऐप जानकारी अपडेट करने में विफल', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'ठीक है', | |||
| cancel: 'रद्द करें', | |||
| }, | |||
| @@ -73,7 +73,7 @@ const translation = { | |||
| editAppTitle: 'Modifica Info App', | |||
| editDone: 'Info app aggiornata', | |||
| editFailed: 'Aggiornamento delle info dell\'app fallito', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Annulla', | |||
| }, | |||
| @@ -72,7 +72,7 @@ const translation = { | |||
| editAppTitle: 'アプリ情報を編集する', | |||
| editDone: 'アプリ情報が更新されました', | |||
| editFailed: 'アプリ情報の更新に失敗しました', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'キャンセル', | |||
| }, | |||
| @@ -63,7 +63,7 @@ const translation = { | |||
| editAppTitle: '앱 정보 편집하기', | |||
| editDone: '앱 정보가 업데이트되었습니다', | |||
| editFailed: '앱 정보 업데이트 실패', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: '확인', | |||
| cancel: '취소', | |||
| }, | |||
| @@ -73,7 +73,7 @@ const translation = { | |||
| editAppTitle: 'Edytuj informacje o aplikacji', | |||
| editDone: 'Informacje o aplikacji zaktualizowane', | |||
| editFailed: 'Nie udało się zaktualizować informacji o aplikacji', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Anuluj', | |||
| }, | |||
| @@ -67,7 +67,7 @@ const translation = { | |||
| editAppTitle: 'Editar Informações do Aplicativo', | |||
| editDone: 'Informações do aplicativo atualizadas', | |||
| editFailed: 'Falha ao atualizar informações do aplicativo', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Cancelar', | |||
| }, | |||
| @@ -67,7 +67,7 @@ const translation = { | |||
| editAppTitle: 'Editează Info Aplicație', | |||
| editDone: 'Informațiile despre aplicație au fost actualizate', | |||
| editFailed: 'Actualizarea informațiilor despre aplicație a eșuat', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Anulează', | |||
| }, | |||
| @@ -67,7 +67,7 @@ const translation = { | |||
| editAppTitle: 'Uygulama Bilgilerini Düzenle', | |||
| editDone: 'Uygulama bilgileri güncellendi', | |||
| editFailed: 'Uygulama bilgileri güncellenemedi', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'Tamam', | |||
| cancel: 'İptal', | |||
| }, | |||
| @@ -67,7 +67,7 @@ const translation = { | |||
| editAppTitle: 'Редагувати інформацію про додаток', | |||
| editDone: 'Інформація про додаток оновлена', | |||
| editFailed: 'Не вдалося оновити інформацію про додаток', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'OK', | |||
| cancel: 'Скасувати', | |||
| }, | |||
| @@ -67,7 +67,7 @@ const translation = { | |||
| editAppTitle: 'Chỉnh sửa thông tin ứng dụng', | |||
| 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', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: 'Đồng ý', | |||
| cancel: 'Hủy', | |||
| }, | |||
| @@ -70,9 +70,11 @@ const translation = { | |||
| editAppTitle: '编辑应用信息', | |||
| editDone: '应用信息已更新', | |||
| editFailed: '更新应用信息失败', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: '确认', | |||
| cancel: '取消', | |||
| emoji: '表情符号', | |||
| image: '图片', | |||
| }, | |||
| switch: '迁移为工作流编排', | |||
| switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将', | |||
| @@ -66,7 +66,7 @@ const translation = { | |||
| editAppTitle: '編輯應用資訊', | |||
| editDone: '應用資訊已更新', | |||
| editFailed: '更新應用資訊失敗', | |||
| emoji: { | |||
| iconPicker: { | |||
| ok: '確認', | |||
| cancel: '取消', | |||
| }, | |||
| @@ -1,5 +1,5 @@ | |||
| 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' | |||
| export enum DataSourceType { | |||
| @@ -425,8 +425,10 @@ export type RelatedApp = { | |||
| id: string | |||
| name: string | |||
| mode: AppMode | |||
| icon_type: AppIconType | null | |||
| icon: string | |||
| icon_background: string | |||
| icon_url: string | |||
| } | |||
| export type RelatedAppResponse = { | |||
| @@ -1,9 +1,11 @@ | |||
| import type { AppMode } from '@/types/app' | |||
| import type { AppIconType, AppMode } from '@/types/app' | |||
| export type AppBasicInfo = { | |||
| id: string | |||
| mode: AppMode | |||
| icon_type: AppIconType | null | |||
| icon: string | |||
| icon_background: string | |||
| icon_url: string | |||
| name: string | |||
| description: string | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| import type { Locale } from '@/i18n' | |||
| import type { AppIconType } from '@/types/app' | |||
| export type ResponseHolder = {} | |||
| @@ -13,8 +14,10 @@ export type SiteInfo = { | |||
| title: string | |||
| chat_color_theme?: string | |||
| chat_color_theme_inverted?: boolean | |||
| icon_type?: AppIconType | |||
| icon?: string | |||
| icon_background?: string | |||
| icon_url?: string | |||
| description?: string | |||
| default_language?: Locale | |||
| prompt_public?: boolean | |||
| @@ -68,6 +68,7 @@ | |||
| "react": "~18.2.0", | |||
| "react-18-input-autosize": "^3.0.0", | |||
| "react-dom": "~18.2.0", | |||
| "react-easy-crop": "^5.0.8", | |||
| "react-error-boundary": "^4.0.2", | |||
| "react-headless-pagination": "^1.1.4", | |||
| "react-hook-form": "^7.51.4", | |||
| @@ -2,7 +2,7 @@ import type { Fetcher } from 'swr' | |||
| 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 { 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' | |||
| export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => { | |||
| @@ -17,32 +17,32 @@ export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = | |||
| 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 }) => { | |||
| 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 }) => { | |||
| 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) => { | |||
| @@ -291,12 +291,16 @@ export type SiteConfig = { | |||
| /** Custom Disclaimer */ | |||
| custom_disclaimer: string | |||
| icon_type: AppIconType | null | |||
| icon: string | |||
| icon_background: string | |||
| icon_background: string | null | |||
| icon_url: string | null | |||
| show_workflow_steps: boolean | |||
| } | |||
| export type AppIconType = 'image' | 'emoji' | |||
| /** | |||
| * App | |||
| */ | |||
| @@ -308,10 +312,17 @@ export type App = { | |||
| /** Description */ | |||
| description: string | |||
| /** Icon */ | |||
| /** | |||
| * Icon Type | |||
| * @default 'emoji' | |||
| */ | |||
| icon_type: AppIconType | null | |||
| /** Icon, stores file ID if icon_type is 'image' */ | |||
| 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: AppMode | |||
| @@ -7015,6 +7015,11 @@ normalize-range@^0.1.2: | |||
| resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" | |||
| 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: | |||
| version "4.0.1" | |||
| resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" | |||
| @@ -7588,6 +7593,14 @@ react-dom@~18.2.0: | |||
| loose-envify "^1.1.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: | |||
| version "3.1.4" | |||
| resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz" | |||
| @@ -8363,16 +8376,7 @@ string-length@^4.0.1: | |||
| char-regex "^1.0.2" | |||
| 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" | |||
| resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" | |||
| integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | |||
| @@ -8440,14 +8444,7 @@ stringify-entities@^4.0.0: | |||
| character-entities-html4 "^2.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" | |||
| resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" | |||
| integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | |||
| @@ -8760,6 +8757,11 @@ tslib@^1.8.1, tslib@^1.9.3: | |||
| resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" | |||
| 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: | |||
| version "2.5.3" | |||
| resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz" | |||
| @@ -9216,7 +9218,7 @@ word-wrap@^1.2.3: | |||
| resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" | |||
| 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" | |||
| resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" | |||
| integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== | |||
| @@ -9234,15 +9236,6 @@ wrap-ansi@^6.2.0: | |||
| string-width "^4.1.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: | |||
| version "8.1.0" | |||
| resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" | |||