mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-12 00:58:57 +08:00
feat: custom app icon (#7196)
Co-authored-by: crazywoola <427733928@qq.com>
This commit is contained in:
parent
a0c689c273
commit
fbf31b5d52
@ -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'),
|
||||
)
|
||||
|
@ -16,3 +16,6 @@ NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Disable Next.js Telemetry (https://nextjs.org/telemetry)
|
||||
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>
|
||||
|
97
web/app/components/base/app-icon-picker/Uploader.tsx
Normal file
97
web/app/components/base/app-icon-picker/Uploader.tsx
Normal file
@ -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
|
43
web/app/components/base/app-icon-picker/hooks.tsx
Normal file
43
web/app/components/base/app-icon-picker/hooks.tsx
Normal file
@ -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,
|
||||
}
|
||||
}
|
139
web/app/components/base/app-icon-picker/index.tsx
Normal file
139
web/app/components/base/app-icon-picker/index.tsx
Normal file
@ -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
|
12
web/app/components/base/app-icon-picker/style.module.css
Normal file
12
web/app/components/base/app-icon-picker/style.module.css
Normal file
@ -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;
|
||||
}
|
98
web/app/components/base/app-icon-picker/utils.ts
Normal file
98
web/app/components/base/app-icon-picker/utils.ts
Normal file
@ -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(
|
||||
const wrapperClassName = classNames(
|
||||
style.appIcon,
|
||||
size !== 'medium' && style[size],
|
||||
rounded && style.rounded,
|
||||
className ?? '',
|
||||
)}
|
||||
style={{
|
||||
background,
|
||||
}}
|
||||
'overflow-hidden',
|
||||
)
|
||||
|
||||
const isValidImageIcon = iconType === 'image' && imageUrl
|
||||
|
||||
return <span
|
||||
className={wrapperClassName}
|
||||
style={{ background: isValidImageIcon ? undefined : (background || '#FFEAD5') }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />)}
|
||||
{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}
|
||||
|
171
web/app/components/base/emoji-picker/Inner.tsx
Normal file
171
web/app/components/base/emoji-picker/Inner.tsx
Normal file
@ -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
|
||||
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>
|
||||
<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.cancel')}
|
||||
{t('app.iconPicker.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={selectedEmoji === ''}
|
||||
disabled={selectedEmoji === '' || !selectedBackground}
|
||||
variant="primary"
|
||||
className='w-full'
|
||||
onClick={() => {
|
||||
onSelect && onSelect(selectedEmoji, selectedBackground)
|
||||
onSelect && onSelect(selectedEmoji, selectedBackground!)
|
||||
}}>
|
||||
{t('app.emoji.ok')}
|
||||
{t('app.iconPicker.ok')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal> : <>
|
||||
</>
|
||||
</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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user