From 1582411f49da5595654754229f7ddd0c159cb88a Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Thu, 8 Aug 2024 14:43:36 +0800 Subject: [PATCH 01/16] feat: app icon enhancements --- .../base/chat/chat-with-history/hooks.tsx | 3 +++ .../components/base/emoji-picker/index.tsx | 17 ++++---------- .../share/text-generation/index.tsx | 5 ++++ web/hooks/use-app-favicon.ts | 23 +++++++++++++++++++ web/utils/emoji.ts | 11 +++++++++ 5 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 web/hooks/use-app-favicon.ts create mode 100644 web/utils/emoji.ts diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index ab8b3648e7dae9..3fa301d268744d 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -37,11 +37,14 @@ import type { import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { useToastContext } from '@/app/components/base/toast' import { changeLanguage } from '@/i18n/i18next-config' +import { useAppFavicon } from '@/hooks/use-app-favicon' 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) + const appData = useMemo(() => { if (isInstalledApp) { const { id, app } = installedAppInfo! diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index f861bcb20c680b..8840f47950c75d 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -3,8 +3,8 @@ import type { ChangeEvent, FC } from 'react' import React, { useState } from 'react' import data from '@emoji-mart/data' -import type { Emoji, EmojiMartData } from '@emoji-mart/data' -import { SearchIndex, init } from 'emoji-mart' +import type { EmojiMartData } from '@emoji-mart/data' +import { init } from 'emoji-mart' import { MagnifyingGlassIcon, } from '@heroicons/react/24/outline' @@ -13,8 +13,8 @@ import s from './style.module.css' 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 { @@ -30,15 +30,6 @@ declare global { init({ data }) -async function search(value: string) { - const emojis: Emoji[] = await SearchIndex.search(value) || [] - - const results = emojis.map((emoji) => { - return emoji.skins[0].native - }) - return results -} - const backgroundColors = [ '#FFEAD5', '#E4FBCC', @@ -105,7 +96,7 @@ const EmojiPicker: FC = ({ } else { setIsSearching(true) - const emojis = await search(e.target.value) + const emojis = await searchEmoji(e.target.value) setSearchedEmojis(emojis) } }} diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index c02e0fb9c0ba88..1bc41a54400ebf 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -36,6 +36,7 @@ import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config' import Toast from '@/app/components/base/toast' import type { VisionFile, VisionSettings } from '@/types/app' import { Resolution, TransferMethod } from '@/types/app' +import { useAppFavicon } from '@/hooks/use-app-favicon' const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. enum TaskStatus { @@ -363,6 +364,8 @@ const TextGeneration: FC = ({ title: installedAppInfo?.app.name, prompt_public: false, copyright: '', + icon: installedAppInfo?.app.icon, + icon_background: installedAppInfo?.app.icon_background, }, plan: 'basic', } @@ -408,6 +411,8 @@ const TextGeneration: FC = ({ } }, [siteInfo?.title, canReplaceLogo]) + useAppFavicon(!isInstalledApp, siteInfo?.icon, siteInfo?.icon_background) + const [isShowResSidebar, { setTrue: doShowResSidebar, setFalse: hideResSidebar }] = useBoolean(false) const showResSidebar = () => { // fix: useClickAway hideResSidebar will close sidebar diff --git a/web/hooks/use-app-favicon.ts b/web/hooks/use-app-favicon.ts new file mode 100644 index 00000000000000..8904b884ced2f4 --- /dev/null +++ b/web/hooks/use-app-favicon.ts @@ -0,0 +1,23 @@ +import { useAsyncEffect } from 'ahooks' +import { appDefaultIconBackground } from '@/config' +import { searchEmoji } from '@/utils/emoji' + +export function useAppFavicon(enable: boolean, icon?: string, icon_background?: string) { + useAsyncEffect(async () => { + if (!enable) + return + const link: HTMLLinkElement = document.querySelector('link[rel*="icon"]') || document.createElement('link') + + // eslint-disable-next-line prefer-template + link.href = 'data:image/svg+xml,' + + '' + + '' + + (icon ? await searchEmoji(icon) : '🤖') + + '' + + '' + + link.rel = 'shortcut icon' + link.type = 'image/svg' + document.getElementsByTagName('head')[0].appendChild(link) + }, [enable, icon, icon_background]) +} diff --git a/web/utils/emoji.ts b/web/utils/emoji.ts new file mode 100644 index 00000000000000..9123f780f25bcd --- /dev/null +++ b/web/utils/emoji.ts @@ -0,0 +1,11 @@ +import { SearchIndex } from 'emoji-mart' +import type { Emoji } from '@emoji-mart/data' + +export async function searchEmoji(value: string) { + const emojis: Emoji[] = await SearchIndex.search(value) || [] + + const results = emojis.map((emoji) => { + return emoji.skins[0].native + }) + return results +} From a938836bef05cd440e79a8c11c79372c10cebdbc Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Mon, 12 Aug 2024 09:24:53 +0000 Subject: [PATCH 02/16] feat(wip): custom app icon --- api/controllers/console/app/app.py | 9 ++ api/fields/app_fields.py | 3 + ...4_08_09_0315-44d0f8e91926_add_icon_type.py | 39 ++++++ api/models/model.py | 8 +- .../components/app/create-app-modal/index.tsx | 29 ++--- .../base/app-icon-picker/Uploader.tsx | 92 ++++++++++++++ .../components/base/app-icon-picker/hooks.tsx | 43 +++++++ .../components/base/app-icon-picker/index.tsx | 114 ++++++++++++++++++ .../base/app-icon-picker/style.module.css | 12 ++ .../components/base/app-icon-picker/utils.ts | 98 +++++++++++++++ web/app/components/base/app-icon/index.tsx | 63 +++++++--- .../components/base/app-icon/style.module.css | 2 +- .../components/base/emoji-picker/index.tsx | 48 ++------ web/i18n/de-DE/app.ts | 2 +- web/i18n/en-US/app.ts | 4 +- web/i18n/es-ES/app.ts | 2 +- web/i18n/fa-IR/app.ts | 2 +- web/i18n/fr-FR/app.ts | 2 +- web/i18n/hi-IN/app.ts | 2 +- web/i18n/it-IT/app.ts | 2 +- web/i18n/ja-JP/app.ts | 2 +- web/i18n/ko-KR/app.ts | 2 +- web/i18n/pl-PL/app.ts | 2 +- web/i18n/pt-BR/app.ts | 2 +- web/i18n/ro-RO/app.ts | 2 +- web/i18n/tr-TR/app.ts | 2 +- web/i18n/uk-UA/app.ts | 2 +- web/i18n/vi-VN/app.ts | 2 +- web/i18n/zh-Hans/app.ts | 4 +- web/i18n/zh-Hant/app.ts | 2 +- web/package.json | 1 + web/service/apps.ts | 4 + web/types/app.ts | 5 + web/yarn.lock | 49 ++++---- 34 files changed, 541 insertions(+), 116 deletions(-) create mode 100644 api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py create mode 100644 web/app/components/base/app-icon-picker/Uploader.tsx create mode 100644 web/app/components/base/app-icon-picker/hooks.tsx create mode 100644 web/app/components/base/app-icon-picker/index.tsx create mode 100644 web/app/components/base/app-icon-picker/style.module.css create mode 100644 web/app/components/base/app-icon-picker/utils.ts diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 2f304b970c6050..d1d117de67958c 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -17,6 +17,8 @@ from libs.login import login_required from services.app_dsl_service import AppDslService from services.app_service import AppService +from core.file.upload_file_parser import UploadFileParser + ALLOW_CREATE_APP_MODES = ['chat', 'agent-chat', 'advanced-chat', 'workflow', 'completion'] @@ -78,6 +80,12 @@ def post(self): return app, 201 +class AppIconPreviewUrlApi(Resource): + def get(self, file_id): + file_id = str(file_id) + return UploadFileParser.get_signed_temp_image_url(file_id) + + class AppImportApi(Resource): @setup_required @login_required @@ -363,6 +371,7 @@ def post(self, app_id): api.add_resource(AppListApi, '/apps') +api.add_resource(AppIconPreviewUrlApi, '/apps/icon-preview-url/') api.add_resource(AppImportApi, '/apps/import') api.add_resource(AppImportFromUrlApi, '/apps/import/url') api.add_resource(AppApi, '/apps/') diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index 94d804a919869f..cb62814b4bd821 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -75,6 +75,7 @@ '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, 'model_config': fields.Nested(model_config_partial_fields, attribute='app_model_config', allow_null=True), @@ -109,6 +110,7 @@ 'code': fields.String, 'title': fields.String, 'icon': fields.String, + 'icon_type': fields.String, 'icon_background': fields.String, 'description': fields.String, 'default_language': fields.String, @@ -130,6 +132,7 @@ 'description': fields.String, 'mode': fields.String(attribute='mode_compatible_with_agent'), 'icon': fields.String, + 'icon_type': fields.String, 'icon_background': fields.String, 'enable_site': fields.Boolean, 'enable_api': fields.Boolean, diff --git a/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py b/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py new file mode 100644 index 00000000000000..4b6dd883f1a503 --- /dev/null +++ b/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py @@ -0,0 +1,39 @@ +"""add icon_type + +Revision ID: 44d0f8e91926 +Revises: eeb2e349e6ac +Create Date: 2024-08-09 03:15:39.557824 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '44d0f8e91926' +down_revision = 'eeb2e349e6ac' +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 ### diff --git a/api/models/model.py b/api/models/model.py index a6f517ea6b181f..37f21e037122af 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -6,7 +6,7 @@ from flask import request from flask_login import UserMixin -from sqlalchemy import Float, func, text +from sqlalchemy import Float, func, text, Enum as DBEnum from configs import dify_config from core.file.tool_file_parser import ToolFileParser @@ -50,6 +50,10 @@ def value_of(cls, value: str) -> 'AppMode': raise ValueError(f'invalid mode value {value}') +class IconType(Enum): + IMAGE = "image" + EMOJI = "emoji" + class App(db.Model): __tablename__ = 'apps' __table_args__ = ( @@ -62,6 +66,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) @@ -1086,6 +1091,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) diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index c4cedbb3543b9f..6c693f0616ee52 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -13,12 +13,12 @@ import cn from '@/utils/classnames' import AppsContext, { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { ToastContext } from '@/app/components/base/toast' -import type { AppMode } from '@/types/app' +import type { AppIconType, AppMode } from '@/types/app' 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 AppIconPicker from "../../base/app-icon-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 +40,8 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { const [appMode, setAppMode] = useState('chat') const [showChatBotType, setShowChatBotType] = useState(true) - const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' }) - const [showEmojiPicker, setShowEmojiPicker] = useState(false) + const [appIcon, setAppIcon] = useState({ type: 'emoji' as AppIconType, icon: '🤖', background: '#FFEAD5' as string | undefined }) + const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [name, setName] = useState('') const [description, setDescription] = useState('') @@ -63,11 +63,12 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { return isCreatingRef.current = true try { + if (appIcon.type === 'image') throw new Error('unimplemented') const app = await createApp({ name, description, - icon: emoji.icon, - icon_background: emoji.icon_background, + icon: appIcon.icon, + icon_background: appIcon.background!, mode: appMode, }) notify({ type: 'success', message: t('app.newApp.appCreated') }) @@ -81,7 +82,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.icon, appIcon.background, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) return ( {
{t('app.newApp.captionName')}
- { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> + { setShowAppIconPicker(true) }} className='cursor-pointer' iconType={appIcon.type} icon={appIcon.icon} background={appIcon.background} /> setName(e.target.value)} @@ -277,14 +278,14 @@ 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' />
- {showEmojiPicker && { - setEmoji({ icon, icon_background }) - setShowEmojiPicker(false) + {showAppIconPicker && { + setAppIcon({ type: iconType, icon, background }) + setShowAppIconPicker(false) }} onClose={() => { - setEmoji({ icon: '🤖', icon_background: '#FFEAD5' }) - setShowEmojiPicker(false) + setAppIcon({ type: 'emoji', icon: '🤖', background: '#FFEAD5' }) + setShowAppIconPicker(false) }} />}
diff --git a/web/app/components/base/app-icon-picker/Uploader.tsx b/web/app/components/base/app-icon-picker/Uploader.tsx new file mode 100644 index 00000000000000..b3f46b596ad5c9 --- /dev/null +++ b/web/app/components/base/app-icon-picker/Uploader.tsx @@ -0,0 +1,92 @@ +'use client' + +import { FC, ChangeEvent, useState, useEffect, createRef } from 'react' +import Cropper, { Area } from 'react-easy-crop' +import classNames from "classnames" + +import { useDraggableUploader } from "./hooks" +import { ImagePlus } from "../icons/src/vender/line/images" +import { ALLOW_FILE_EXTENSIONS, ImageFile } from "@/types/app" + +interface UploaderProps { + className?: string + onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void +} + +const Uploader: FC = ({ + 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) => { + 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() + + return ( +
+
+ { + !inputImage + ? <> + +
+ Drop your image here, or  + + ((e.target as HTMLInputElement).value = '')} + accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} + onChange={handleLocalFileInput} + /> +
+
Supports PNG, JPG, JPEG, WEBP and GIF
+ + : + } +
+
+ ) +} + +export default Uploader \ No newline at end of file diff --git a/web/app/components/base/app-icon-picker/hooks.tsx b/web/app/components/base/app-icon-picker/hooks.tsx new file mode 100644 index 00000000000000..a164241bba902e --- /dev/null +++ b/web/app/components/base/app-icon-picker/hooks.tsx @@ -0,0 +1,43 @@ +import { useCallback, useState } from "react" + +export const useDraggableUploader = (setImageFn: (file: File) => void) => { + const [isDragActive, setIsDragActive] = useState(false) + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(true) + }, []) + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + }, []) + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + + const file = e.dataTransfer.files[0] + + if (!file) + return + + setImageFn(file) + }, []) + + return { + handleDragEnter, + handleDragOver, + handleDragLeave, + handleDrop, + isDragActive, + } +} diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx new file mode 100644 index 00000000000000..48bcb0a20de86d --- /dev/null +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -0,0 +1,114 @@ +import { FC, useState } from "react" +import { useTranslation } from "react-i18next" +import Modal from "../modal" +import cn from '@/utils/classnames' +import s from './style.module.css' +import Divider from "../divider" +import Button from "../button" +import { ImagePlus } from "../icons/src/vender/line/images" +import EmojiPicker from "../emoji-picker" +import Uploader from "./Uploader" +import { AppIconType, ImageFile } from "@/types/app" +import { useLocalFileUploader } from "../image-uploader/hooks" +import { Area } from "react-easy-crop" +import getCroppedImg from "./utils" + +interface AppIconPickerProps { + onSelect?: (iconType: AppIconType, icon: string, background?: string) => void + onClose?: () => void + className?: string +} + +const AppIconPicker: FC = ({ + onSelect, + onClose, + className, +}) => { + const { t } = useTranslation() + + const tabs = [ + { key: 'emoji', label: t(`app.iconPicker.emoji`), icon: 🤖 }, + { key: 'image', label: t(`app.iconPicker.image`), icon: }, + ] + const [activeTab, setActiveTab] = useState('emoji') + + const [emoji, setEmoji] = useState<{ emoji: string, background: string }>() + const handleSelectEmoji = (emoji: string, background: string) => { + setEmoji({ emoji, background }) + } + + const [uploading, setUploading] = useState() + + const { handleLocalFileUpload } = useLocalFileUploader({ + limit: 3, + disabled: false, + onUpload: (imageFile: ImageFile) => { + if (imageFile.fileId) { + setUploading(false) + onSelect?.('image', imageFile.fileId) + } + } + }) + + 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?.('emoji', emoji.emoji, 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 { }} + isShow + closable={false} + wrapperClassName={className} + className={cn(s.container, '!w-[362px] !p-0')} + > +
+
+ {tabs.map(tab => ( + + ))} +
+
+ + + + + + + +
+ + + +
+
+} + +export default AppIconPicker \ No newline at end of file diff --git a/web/app/components/base/app-icon-picker/style.module.css b/web/app/components/base/app-icon-picker/style.module.css new file mode 100644 index 00000000000000..5facb3560a04d3 --- /dev/null +++ b/web/app/components/base/app-icon-picker/style.module.css @@ -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; +} diff --git a/web/app/components/base/app-icon-picker/utils.ts b/web/app/components/base/app-icon-picker/utils.ts new file mode 100644 index 00000000000000..3fd2f970f4241a --- /dev/null +++ b/web/app/components/base/app-icon-picker/utils.ts @@ -0,0 +1,98 @@ +export const createImage = (url: string) => + new Promise((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 { + 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('Error') + }, 'image/jpeg') + }) +} diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 9d8cf28beda7a1..9b6feb91c9bec8 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -1,15 +1,20 @@ -import type { FC } from 'react' +'use client' -import data from '@emoji-mart/data' +import { PropsWithChildren, useState, type FC } from 'react' +import { useAsyncEffect } from "ahooks" import { init } from 'emoji-mart' +import data from '@emoji-mart/data' import style from './style.module.css' import classNames from '@/utils/classnames' +import { AppIconType } from "@/types/app" +import { fetchAppIconPreviewUrl } from "@/service/apps" init({ data }) export type AppIconProps = { size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' rounded?: boolean + iconType?: AppIconType icon?: string background?: string className?: string @@ -17,31 +22,53 @@ export type AppIconProps = { onClick?: () => void } +const AppIconWrapper = ({ + size = 'medium', + rounded = false, + background, + className, + onClick, + children +}: PropsWithChildren>) => { + const wrapperClassName = classNames( + style.appIcon, + size !== 'medium' && style[size], + rounded && style.rounded, + className ?? '', + ) + return {children} +} + const AppIcon: FC = ({ size = 'medium', rounded = false, + iconType = 'emoji', icon, background, className, innerIcon, onClick, }) => { - return ( - - {innerIcon || ((icon && icon !== '') ? : )} - - ) + const [imageIconSrc, setImageIconSrc] = useState('') + const [loading, setLoading] = useState(true) + + useAsyncEffect(async () => { + if (iconType === 'image' && icon) { + setLoading(true) + const res = await fetchAppIconPreviewUrl({ fileID: icon }) + setImageIconSrc(res) + setLoading(false) + } + }, [iconType, icon]) + + return + {iconType === 'emoji' + ? innerIcon || ((icon && icon !== '') ? : ) + : loading + ? '' + : + } + } export default AppIcon diff --git a/web/app/components/base/app-icon/style.module.css b/web/app/components/base/app-icon/style.module.css index 20f76d40995daa..06a2478d41e380 100644 --- a/web/app/components/base/app-icon/style.module.css +++ b/web/app/components/base/app-icon/style.module.css @@ -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 { diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index 8840f47950c75d..5241862cdb9d03 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -8,21 +8,18 @@ import { init } from 'emoji-mart' import { MagnifyingGlassIcon, } from '@heroicons/react/24/outline' -import { useTranslation } from 'react-i18next' -import s from './style.module.css' 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' +import classNames from "@/utils/classnames" declare global { namespace JSX { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface IntrinsicElements { 'em-emoji': React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement + React.HTMLAttributes, + HTMLElement > } } @@ -53,19 +50,14 @@ const backgroundColors = [ ] type IEmojiPickerProps = { - isModal?: boolean onSelect?: (emoji: string, background: string) => void - onClose?: () => void className?: string } const EmojiPicker: FC = ({ - isModal = true, onSelect, - onClose, className, }) => { - const { t } = useTranslation() const { categories } = data as EmojiMartData const [selectedEmoji, setSelectedEmoji] = useState('') const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0]) @@ -73,13 +65,13 @@ const EmojiPicker: FC = ({ const [searchedEmojis, setSearchedEmojis] = useState([]) const [isSearching, setIsSearching] = useState(false) - return isModal ? { }} - isShow - closable={false} - wrapperClassName={className} - className={cn(s.container, '!w-[362px] !p-0')} - > + React.useEffect(() => { + if (selectedEmoji && selectedBackground) { + onSelect?.(selectedEmoji, selectedBackground) + } + }, [selectedEmoji, selectedBackground]) + + return
@@ -178,24 +170,6 @@ const EmojiPicker: FC = ({ })}
- -
- - -
- : <> - +
} export default EmojiPicker diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index dc7396702c2306..6df1eba81ef964 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -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', }, diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index 1c70618a4fa6ce..39b47c8eb4e513 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -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 ', diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts index 82359b40b0bd26..c3d7a8362b2142 100644 --- a/web/i18n/es-ES/app.ts +++ b/web/i18n/es-ES/app.ts @@ -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', }, diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index 8322ff5a14a158..9f2563709e4e80 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -71,7 +71,7 @@ const translation = { editAppTitle: 'ویرایش اطلاعات برنامه', editDone: 'اطلاعات برنامه به‌روزرسانی شد', editFailed: 'به‌روزرسانی اطلاعات برنامه ناموفق بود', - emoji: { + iconPicker: { ok: 'باشه', cancel: 'لغو', }, diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index 8e794715761c4c..aaea1b17183dff 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -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', }, diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index 9286f74c1c16b2..0ae669cdb7f9c0 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -67,7 +67,7 @@ const translation = { editAppTitle: 'ऐप जानकारी संपादित करें', editDone: 'ऐप जानकारी अपडेट की गई', editFailed: 'ऐप जानकारी अपडेट करने में विफल', - emoji: { + iconPicker: { ok: 'ठीक है', cancel: 'रद्द करें', }, diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index c58149ef84a70d..3c23c3ea94d550 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -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', }, diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index 91bca235f5f3a5..043f58b83c233d 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -72,7 +72,7 @@ const translation = { editAppTitle: 'アプリ情報を編集する', editDone: 'アプリ情報が更新されました', editFailed: 'アプリ情報の更新に失敗しました', - emoji: { + iconPicker: { ok: 'OK', cancel: 'キャンセル', }, diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index d9fb7b378852d4..d938098e90acee 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -63,7 +63,7 @@ const translation = { editAppTitle: '앱 정보 편집하기', editDone: '앱 정보가 업데이트되었습니다', editFailed: '앱 정보 업데이트 실패', - emoji: { + iconPicker: { ok: '확인', cancel: '취소', }, diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index 1bf6e2f3764943..c292a4c7404329 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -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', }, diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index e12a2b62fa3558..6c6c5f85154d16 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -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', }, diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index 333678863fb166..3d8b8daea1beb1 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -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ă', }, diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 600b1faecb86ae..336d3567d14cce 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -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', }, diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 209bf12598c7a9..e1c5844a597964 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -67,7 +67,7 @@ const translation = { editAppTitle: 'Редагувати інформацію про додаток', editDone: 'Інформація про додаток оновлена', editFailed: 'Не вдалося оновити інформацію про додаток', - emoji: { + iconPicker: { ok: 'OK', cancel: 'Скасувати', }, diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index ca7eedc434813d..685b140e30c0d0 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -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', }, diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 002284b91b3810..6703e1ca95a8ae 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -70,9 +70,11 @@ const translation = { editAppTitle: '编辑应用信息', editDone: '应用信息已更新', editFailed: '更新应用信息失败', - emoji: { + iconPicker: { ok: '确认', cancel: '取消', + emoji: '表情符号', + image: '图片', }, switch: '迁移为工作流编排', switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将', diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index dfdc0ea7af853a..df7f601bb2282e 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -66,7 +66,7 @@ const translation = { editAppTitle: '編輯應用資訊', editDone: '應用資訊已更新', editFailed: '更新應用資訊失敗', - emoji: { + iconPicker: { ok: '確認', cancel: '取消', }, diff --git a/web/package.json b/web/package.json index 2d6cd0a511e565..13ef29cdb2d3ae 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/service/apps.ts b/web/service/apps.ts index 3c12de5c6fbcdf..6de9250cdaa27a 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -9,6 +9,10 @@ export const fetchAppList: Fetcher(url, { params }) } +export const fetchAppIconPreviewUrl = ({ fileID }: { fileID: string }) => { + return get(`/apps/icon-preview-url/${fileID}`) +} + export const fetchAppDetail = ({ url, id }: { url: string; id: string }) => { return get(`${url}/${id}`) } diff --git a/web/types/app.ts b/web/types/app.ts index ebf20f2d686b22..1d827d6319730e 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -6,6 +6,7 @@ import type { RerankingModeEnum, WeightedScoreEnum, } from '@/models/datasets' +import { AppIconType } from '@/app/components/base/app-icon-picker' export enum Theme { light = 'light', @@ -297,6 +298,8 @@ export type SiteConfig = { show_workflow_steps: boolean } +export type AppIconType = 'image' | 'emoji' + /** * App */ @@ -308,6 +311,8 @@ export type App = { /** Description */ description: string + /** Icon Type */ + iconType: AppIconType /** Icon */ icon: string /** Icon Background */ diff --git a/web/yarn.lock b/web/yarn.lock index 16207b6ee07e18..16f672ae34110b 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -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" From a1fa936802ee3044231760c6bdf7cdac19c7c368 Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Mon, 12 Aug 2024 18:02:56 +0800 Subject: [PATCH 03/16] fix: lint error --- .../components/app/create-app-modal/index.tsx | 7 +- .../base/app-icon-picker/Uploader.tsx | 157 +++++++-------- .../components/base/app-icon-picker/hooks.tsx | 80 ++++---- .../components/base/app-icon-picker/index.tsx | 184 +++++++++--------- .../components/base/app-icon-picker/utils.ts | 156 +++++++-------- web/app/components/base/app-icon/index.tsx | 16 +- .../components/base/emoji-picker/index.tsx | 14 +- web/types/app.ts | 1 - 8 files changed, 309 insertions(+), 306 deletions(-) diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 6c693f0616ee52..38acf738a6da5d 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -8,6 +8,7 @@ import { } from '@remixicon/react' import { useRouter } from 'next/navigation' import { useContext, useContextSelector } from 'use-context-selector' +import AppIconPicker 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 +19,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 AppIconPicker from "../../base/app-icon-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' @@ -63,7 +63,8 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { return isCreatingRef.current = true try { - if (appIcon.type === 'image') throw new Error('unimplemented') + if (appIcon.type === 'image') + throw new Error('unimplemented') const app = await createApp({ name, description, @@ -82,7 +83,7 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) } isCreatingRef.current = false - }, [name, notify, t, appMode, appIcon.icon, appIcon.background, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) + }, [name, notify, t, appMode, appIcon.icon, appIcon.background, appIcon.type, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) return ( void +type UploaderProps = { + className?: string + onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void } const Uploader: FC = ({ - className, - onImageCropped, + className, + onImageCropped, }) => { - const [inputImage, setInputImage] = useState<{ file: File, url: string }>() - useEffect(() => { - return () => { - if (inputImage) URL.revokeObjectURL(inputImage.url); - } - }, [inputImage]) + 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 [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 onCropComplete = async (_: Area, croppedAreaPixels: Area) => { + if (!inputImage) + return + onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name) + } - const handleLocalFileInput = (e: ChangeEvent) => { - const file = e.target.files?.[0] - if (file) setInputImage({ file, url: URL.createObjectURL(file) }) - } + const handleLocalFileInput = (e: ChangeEvent) => { + 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 { + isDragActive, + handleDragEnter, + handleDragOver, + handleDragLeave, + handleDrop, + } = useDraggableUploader((file: File) => setInputImage({ file, url: URL.createObjectURL(file) })) - const inputRef = createRef() + const inputRef = createRef() - return ( -
-
- { - !inputImage - ? <> - -
- Drop your image here, or  - - ((e.target as HTMLInputElement).value = '')} - accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} - onChange={handleLocalFileInput} - /> -
-
Supports PNG, JPG, JPEG, WEBP and GIF
- - : - } -
-
- ) + return ( +
+
+ { + !inputImage + ? <> + +
+ Drop your image here, or  + + ((e.target as HTMLInputElement).value = '')} + accept={ALLOW_FILE_EXTENSIONS.map(ext => `.${ext}`).join(',')} + onChange={handleLocalFileInput} + /> +
+
Supports PNG, JPG, JPEG, WEBP and GIF
+ + : + } +
+
+ ) } -export default Uploader \ No newline at end of file +export default Uploader diff --git a/web/app/components/base/app-icon-picker/hooks.tsx b/web/app/components/base/app-icon-picker/hooks.tsx index a164241bba902e..b3f67c0dcae930 100644 --- a/web/app/components/base/app-icon-picker/hooks.tsx +++ b/web/app/components/base/app-icon-picker/hooks.tsx @@ -1,43 +1,43 @@ -import { useCallback, useState } from "react" +import { useCallback, useState } from 'react' export const useDraggableUploader = (setImageFn: (file: File) => void) => { - const [isDragActive, setIsDragActive] = useState(false) - - const handleDragEnter = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragActive(true) - }, []) - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - }, []) - - const handleDragLeave = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragActive(false) - }, []) - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragActive(false) - - const file = e.dataTransfer.files[0] - - if (!file) - return - - setImageFn(file) - }, []) - - return { - handleDragEnter, - handleDragOver, - handleDragLeave, - handleDrop, - isDragActive, - } + const [isDragActive, setIsDragActive] = useState(false) + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(true) + }, []) + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + }, []) + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + + const file = e.dataTransfer.files[0] + + if (!file) + return + + setImageFn(file) + }, [setImageFn]) + + return { + handleDragEnter, + handleDragOver, + handleDragLeave, + handleDrop, + isDragActive, + } } diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index 48bcb0a20de86d..ba1fd83c64a8d2 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -1,114 +1,116 @@ -import { FC, useState } from "react" -import { useTranslation } from "react-i18next" -import Modal from "../modal" -import cn from '@/utils/classnames' +import type { FC } from 'react' +import { 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 EmojiPicker from '../emoji-picker' +import { useLocalFileUploader } from '../image-uploader/hooks' +import Uploader from './Uploader' import s from './style.module.css' -import Divider from "../divider" -import Button from "../button" -import { ImagePlus } from "../icons/src/vender/line/images" -import EmojiPicker from "../emoji-picker" -import Uploader from "./Uploader" -import { AppIconType, ImageFile } from "@/types/app" -import { useLocalFileUploader } from "../image-uploader/hooks" -import { Area } from "react-easy-crop" -import getCroppedImg from "./utils" +import getCroppedImg from './utils' +import type { AppIconType, ImageFile } from '@/types/app' +import cn from '@/utils/classnames' -interface AppIconPickerProps { - onSelect?: (iconType: AppIconType, icon: string, background?: string) => void - onClose?: () => void - className?: string +type AppIconPickerProps = { + onSelect?: (iconType: AppIconType, icon: string, background?: string) => void + onClose?: () => void + className?: string } const AppIconPicker: FC = ({ - onSelect, - onClose, - className, + onSelect, + onClose, + className, }) => { - const { t } = useTranslation() + const { t } = useTranslation() - const tabs = [ - { key: 'emoji', label: t(`app.iconPicker.emoji`), icon: 🤖 }, - { key: 'image', label: t(`app.iconPicker.image`), icon: }, - ] - const [activeTab, setActiveTab] = useState('emoji') + const tabs = [ + { key: 'emoji', label: t('app.iconPicker.emoji'), icon: 🤖 }, + { key: 'image', label: t('app.iconPicker.image'), icon: }, + ] + const [activeTab, setActiveTab] = useState('emoji') - const [emoji, setEmoji] = useState<{ emoji: string, background: string }>() - const handleSelectEmoji = (emoji: string, background: string) => { - setEmoji({ emoji, background }) - } + const [emoji, setEmoji] = useState<{ emoji: string; background: string }>() + const handleSelectEmoji = (emoji: string, background: string) => { + setEmoji({ emoji, background }) + } - const [uploading, setUploading] = useState() + const [uploading, setUploading] = useState() - const { handleLocalFileUpload } = useLocalFileUploader({ - limit: 3, - disabled: false, - onUpload: (imageFile: ImageFile) => { - if (imageFile.fileId) { - setUploading(false) - onSelect?.('image', imageFile.fileId) - } - } - }) + const { handleLocalFileUpload } = useLocalFileUploader({ + limit: 3, + disabled: false, + onUpload: (imageFile: ImageFile) => { + if (imageFile.fileId) { + setUploading(false) + onSelect?.('image', imageFile.fileId) + } + }, + }) - const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string, croppedAreaPixels: Area, fileName: string }>() - const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => { - setImageCropInfo({ tempUrl, croppedAreaPixels, fileName }) - } + 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?.('emoji', emoji.emoji, 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) - } + const handleSelect = async () => { + if (activeTab === 'emoji') { + if (emoji) + onSelect?.('emoji', emoji.emoji, 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 { }} - isShow - closable={false} - wrapperClassName={className} - className={cn(s.container, '!w-[362px] !p-0')} - > -
-
- {tabs.map(tab => ( - - ))} -
-
+ onClick={() => setActiveTab(tab.key as AppIconType)} + > + {tab.icon}   {tab.label} + + ))} +
+ - + - - + + - -
- + +
+ - -
- + +
+
} -export default AppIconPicker \ No newline at end of file +export default AppIconPicker diff --git a/web/app/components/base/app-icon-picker/utils.ts b/web/app/components/base/app-icon-picker/utils.ts index 3fd2f970f4241a..0c90e96febda83 100644 --- a/web/app/components/base/app-icon-picker/utils.ts +++ b/web/app/components/base/app-icon-picker/utils.ts @@ -1,98 +1,98 @@ export const createImage = (url: string) => - new Promise((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 - }) + new Promise((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 + 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) + const rotRad = getRadianAngle(rotation) - return { - width: + return { + width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height), - 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 } + imageSrc: string, + pixelCrop: { x: number; y: number; width: number; height: number }, + rotation = 0, + flip = { horizontal: false, vertical: false }, ): Promise { - 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('Error') - }, 'image/jpeg') - }) + 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') + }) } diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 9b6feb91c9bec8..96bd7af146da4d 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -1,13 +1,15 @@ 'use client' -import { PropsWithChildren, useState, type FC } from 'react' -import { useAsyncEffect } from "ahooks" +import { useState } from 'react' +import type { FC, PropsWithChildren } from 'react' +import { useAsyncEffect } from 'ahooks' +import Image from 'next/image' import { init } from 'emoji-mart' import data from '@emoji-mart/data' import style from './style.module.css' import classNames from '@/utils/classnames' -import { AppIconType } from "@/types/app" -import { fetchAppIconPreviewUrl } from "@/service/apps" +import type { AppIconType } from '@/types/app' +import { fetchAppIconPreviewUrl } from '@/service/apps' init({ data }) @@ -28,7 +30,7 @@ const AppIconWrapper = ({ background, className, onClick, - children + children, }: PropsWithChildren>) => { const wrapperClassName = classNames( style.appIcon, @@ -63,10 +65,10 @@ const AppIcon: FC = ({ return {iconType === 'emoji' - ? innerIcon || ((icon && icon !== '') ? : ) + ? (innerIcon || ((icon && icon !== '') ? : )) : loading ? '' - : + : App icon } } diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index 5241862cdb9d03..02aa1ea5769caf 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -1,4 +1,3 @@ -/* eslint-disable multiline-ternary */ 'use client' import type { ChangeEvent, FC } from 'react' import React, { useState } from 'react' @@ -11,16 +10,12 @@ import { import cn from '@/utils/classnames' import Divider from '@/app/components/base/divider' import { searchEmoji } from '@/utils/emoji' -import classNames from "@/utils/classnames" declare global { namespace JSX { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface IntrinsicElements { - 'em-emoji': React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement - > + 'em-emoji': React.DetailedHTMLProps< React.HTMLAttributes, HTMLElement > } } } @@ -66,12 +61,11 @@ const EmojiPicker: FC = ({ const [isSearching, setIsSearching] = useState(false) React.useEffect(() => { - if (selectedEmoji && selectedBackground) { + if (selectedEmoji && selectedBackground) onSelect?.(selectedEmoji, selectedBackground) - } - }, [selectedEmoji, selectedBackground]) + }, [onSelect, selectedEmoji, selectedBackground]) - return
+ return
diff --git a/web/types/app.ts b/web/types/app.ts index 1d827d6319730e..ed74182793cbad 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -6,7 +6,6 @@ import type { RerankingModeEnum, WeightedScoreEnum, } from '@/models/datasets' -import { AppIconType } from '@/app/components/base/app-icon-picker' export enum Theme { light = 'light', From a389d0c54d30b341b670ae1c8e16e372b960d02b Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Tue, 13 Aug 2024 10:47:13 +0800 Subject: [PATCH 04/16] chore: simplify image selection logic --- api/controllers/console/app/app.py | 9 ---- .../components/app/create-app-modal/index.tsx | 20 +++++--- .../components/base/app-icon-picker/index.tsx | 31 ++++++++++-- web/app/components/base/app-icon/index.tsx | 47 ++++--------------- 4 files changed, 50 insertions(+), 57 deletions(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index d1d117de67958c..2f304b970c6050 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -17,8 +17,6 @@ from libs.login import login_required from services.app_dsl_service import AppDslService from services.app_service import AppService -from core.file.upload_file_parser import UploadFileParser - ALLOW_CREATE_APP_MODES = ['chat', 'agent-chat', 'advanced-chat', 'workflow', 'completion'] @@ -80,12 +78,6 @@ def post(self): return app, 201 -class AppIconPreviewUrlApi(Resource): - def get(self, file_id): - file_id = str(file_id) - return UploadFileParser.get_signed_temp_image_url(file_id) - - class AppImportApi(Resource): @setup_required @login_required @@ -371,7 +363,6 @@ def post(self, app_id): api.add_resource(AppListApi, '/apps') -api.add_resource(AppIconPreviewUrlApi, '/apps/icon-preview-url/') api.add_resource(AppImportApi, '/apps/import') api.add_resource(AppImportFromUrlApi, '/apps/import/url') api.add_resource(AppApi, '/apps/') diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 38acf738a6da5d..d2b7c0dabe2ce1 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -9,12 +9,13 @@ import { 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' import { useProviderContext } from '@/context/provider-context' import { ToastContext } from '@/app/components/base/toast' -import type { AppIconType, AppMode } from '@/types/app' +import type { AppMode } from '@/types/app' import { createApp } from '@/service/apps' import Modal from '@/app/components/base/modal' import Button from '@/app/components/base/button' @@ -40,7 +41,7 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { const [appMode, setAppMode] = useState('chat') const [showChatBotType, setShowChatBotType] = useState(true) - const [appIcon, setAppIcon] = useState({ type: 'emoji' as AppIconType, icon: '🤖', background: '#FFEAD5' as string | undefined }) + const [appIcon, setAppIcon] = useState({ type: 'emoji', icon: '🤖', background: '#FFEAD5' }) const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [name, setName] = useState('') const [description, setDescription] = useState('') @@ -83,7 +84,7 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) } isCreatingRef.current = false - }, [name, notify, t, appMode, appIcon.icon, appIcon.background, appIcon.type, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) + }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor]) return ( {
{t('app.newApp.captionName')}
- { setShowAppIconPicker(true) }} className='cursor-pointer' iconType={appIcon.type} icon={appIcon.icon} background={appIcon.background} /> + { setShowAppIconPicker(true) }} + /> setName(e.target.value)} @@ -280,12 +287,11 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { />
{showAppIconPicker && { - setAppIcon({ type: iconType, icon, background }) + onSelect={(payload) => { + setAppIcon(payload) setShowAppIconPicker(false) }} onClose={() => { - setAppIcon({ type: 'emoji', icon: '🤖', background: '#FFEAD5' }) setShowAppIconPicker(false) }} />} diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index ba1fd83c64a8d2..f02e260ce45c1d 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -14,8 +14,22 @@ import getCroppedImg from './utils' import type { AppIconType, ImageFile } from '@/types/app' import cn from '@/utils/classnames' +type AppIconEmojiSelection = { + type: 'emoji' + icon: string + background: string +} + +type AppIconImageSelection = { + type: 'image' + fileId: string + url: string +} + +export type AppIconSelection = AppIconEmojiSelection | AppIconImageSelection + type AppIconPickerProps = { - onSelect?: (iconType: AppIconType, icon: string, background?: string) => void + onSelect?: (payload: AppIconSelection) => void onClose?: () => void className?: string } @@ -46,7 +60,11 @@ const AppIconPicker: FC = ({ onUpload: (imageFile: ImageFile) => { if (imageFile.fileId) { setUploading(false) - onSelect?.('image', imageFile.fileId) + onSelect?.({ + type: 'image', + fileId: imageFile.fileId, + url: imageFile.url, + }) } }, }) @@ -58,8 +76,13 @@ const AppIconPicker: FC = ({ const handleSelect = async () => { if (activeTab === 'emoji') { - if (emoji) - onSelect?.('emoji', emoji.emoji, emoji.background) + if (emoji) { + onSelect?.({ + type: 'emoji', + icon: emoji.emoji, + background: emoji.background, + }) + } } else { if (!imageCropInfo) diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 96bd7af146da4d..9f7b65da27847a 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -1,15 +1,11 @@ 'use client' -import { useState } from 'react' -import type { FC, PropsWithChildren } from 'react' -import { useAsyncEffect } from 'ahooks' -import Image from 'next/image' +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' -import { fetchAppIconPreviewUrl } from '@/service/apps' init({ data }) @@ -24,53 +20,30 @@ export type AppIconProps = { onClick?: () => void } -const AppIconWrapper = ({ +const AppIcon: FC = ({ size = 'medium', rounded = false, + iconType = 'emoji', + icon, background, className, + innerIcon, onClick, - children, -}: PropsWithChildren>) => { +}) => { const wrapperClassName = classNames( style.appIcon, size !== 'medium' && style[size], rounded && style.rounded, className ?? '', + 'overflow-hidden', ) - return {children} -} - -const AppIcon: FC = ({ - size = 'medium', - rounded = false, - iconType = 'emoji', - icon, - background, - className, - innerIcon, - onClick, -}) => { - const [imageIconSrc, setImageIconSrc] = useState('') - const [loading, setLoading] = useState(true) - - useAsyncEffect(async () => { - if (iconType === 'image' && icon) { - setLoading(true) - const res = await fetchAppIconPreviewUrl({ fileID: icon }) - setImageIconSrc(res) - setLoading(false) - } - }, [iconType, icon]) - return + return {iconType === 'emoji' ? (innerIcon || ((icon && icon !== '') ? : )) - : loading - ? '' - : App icon + : app icon } - + } export default AppIcon From 06264cc029737611d5495d9283a88797e9084dc5 Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Tue, 13 Aug 2024 10:48:09 +0800 Subject: [PATCH 05/16] fix: python lint err --- .../versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py | 4 ++-- api/models/model.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py b/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py index 4b6dd883f1a503..2b6b452ab27e49 100644 --- a/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py +++ b/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py @@ -5,10 +5,10 @@ Create Date: 2024-08-09 03:15:39.557824 """ -from alembic import op -import models as models import sqlalchemy as sa +from alembic import op +import models as models # revision identifiers, used by Alembic. revision = '44d0f8e91926' diff --git a/api/models/model.py b/api/models/model.py index a4d3c743bde74d..7f837a313f8d57 100644 --- a/api/models/model.py +++ b/api/models/model.py @@ -6,7 +6,7 @@ from flask import request from flask_login import UserMixin -from sqlalchemy import Float, func, text, Enum as DBEnum +from sqlalchemy import Float, func, text from configs import dify_config from core.file.tool_file_parser import ToolFileParser From 0bfedac57202ef80f562fde39ba3093ad3cab590 Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Wed, 14 Aug 2024 10:13:32 +0800 Subject: [PATCH 06/16] chore: wip --- api/controllers/console/app/app.py | 2 + api/fields/app_fields.py | 8 +++- api/fields/installed_app_fields.py | 6 ++- api/libs/helper.py | 8 ++++ api/services/app_dsl_service.py | 7 +++ api/services/app_service.py | 1 + web/app/(commonLayout)/apps/AppCard.tsx | 7 ++- .../[datasetId]/layout.tsx | 2 +- web/app/components/app-sidebar/app-info.tsx | 21 ++++++-- .../tools/external-data-tool-modal.tsx | 1 + .../app/configuration/tools/index.tsx | 1 + .../components/app/create-app-modal/index.tsx | 10 ++-- .../components/app/duplicate-modal/index.tsx | 48 ++++++++++++++----- web/app/components/base/app-icon/index.tsx | 20 +++++--- web/app/components/explore/app-card/index.tsx | 8 +++- .../explore/sidebar/app-nav-item/index.tsx | 9 +++- web/app/components/explore/sidebar/index.tsx | 4 +- web/models/datasets.ts | 4 +- web/models/explore.ts | 4 +- web/service/apps.ts | 10 ++-- web/types/app.ts | 8 ++-- 21 files changed, 143 insertions(+), 46 deletions(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 2f304b970c6050..60925ce1855950 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -61,6 +61,7 @@ def post(self): 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() @@ -208,6 +209,7 @@ def post(self, app_model): 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() diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index cb62814b4bd821..6c5cd4f386031a 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -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 = { @@ -78,6 +80,7 @@ '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)) @@ -131,9 +134,10 @@ 'name': fields.String, 'description': fields.String, 'mode': fields.String(attribute='mode_compatible_with_agent'), - 'icon': fields.String, '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), diff --git a/api/fields/installed_app_fields.py b/api/fields/installed_app_fields.py index 35cc5a64755eca..6fc5f6ca4d01fa 100644 --- a/api/fields/installed_app_fields.py +++ b/api/fields/installed_app_fields.py @@ -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_background': fields.String, + 'icon_url': AppIconUrlField, } installed_app_fields = { diff --git a/api/libs/helper.py b/api/libs/helper.py index 15cd65dd6a38ba..d8e39c35d6234a 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -16,6 +16,7 @@ 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 @@ -23,6 +24,13 @@ 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 + if obj.icon_type == 'image': + return UploadFileParser.get_signed_temp_image_url(obj.icon) + return None class TimestampField(fields.Raw): def format(self, value) -> int: diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 9d78037c347806..040a71d1f3a968 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -82,6 +82,7 @@ def import_and_create_new_app(cls, tenant_id: str, data: str, args: dict, accoun # 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 @@ def import_and_create_new_app(cls, tenant_id: str, data: str, args: dict, accoun account=account, name=name, description=description, + icon_type=icon_type, icon=icon, icon_background=icon_background ) @@ -107,6 +109,7 @@ def import_and_create_new_app(cls, tenant_id: str, data: str, args: dict, accoun account=account, name=name, description=description, + icon_type=icon_type, icon=icon, icon_background=icon_background ) @@ -301,6 +304,7 @@ def _import_and_create_new_model_config_based_app(cls, account: Account, name: str, description: str, + icon_type: str, icon: str, icon_background: str) -> App: """ @@ -325,6 +329,7 @@ def _import_and_create_new_model_config_based_app(cls, account=account, name=name, description=description, + icon_type=icon_type, icon=icon, icon_background=icon_background ) @@ -352,6 +357,7 @@ def _create_app(cls, account: Account, name: str, description: str, + icon_type: str, icon: str, icon_background: str) -> App: """ @@ -370,6 +376,7 @@ def _create_app(cls, mode=app_mode.value, name=name, description=description, + icon_type=icon_type, icon=icon, icon_background=icon_background, enable_site=True, diff --git a/api/services/app_service.py b/api/services/app_service.py index e433bb59bbe994..c084c30fcc4bfa 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -119,6 +119,7 @@ def create_app(self, tenant_id: str, args: dict, account: Account) -> App: 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 diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 1387099a627e1c..d7fd96869e7882 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -101,11 +101,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 +259,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
{app.mode === 'advanced-chat' && ( @@ -372,8 +375,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { {showDuplicateModal && ( setShowDuplicateModal(false)} diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx index 11893ec9de4e08..7cd083d11b2d42 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx @@ -60,7 +60,7 @@ const LikedItem = ({ return (
- + {type === 'app' && ( {detail.mode === 'advanced-chat' && ( diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index 698846cae5144b..c6bd0d1e3314a5 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -86,13 +86,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 +195,13 @@ const AppInfo = ({ expand }: IAppInfoProps) => { >
- + { {/* header */}
- + {appDetail.mode === 'advanced-chat' && ( @@ -414,8 +427,10 @@ const AppInfo = ({ expand }: IAppInfoProps) => { {showDuplicateModal && ( setShowDuplicateModal(false)} diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index 3187990609e691..90314a5d165ac9 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -220,6 +220,7 @@ const ExternalDataToolModal: FC = ({ { setShowEmojiPicker(true) }} className='!w-9 !h-9 rounded-lg border-[0.5px] border-black/5 cursor-pointer ' + iconType="emoji" icon={localeData.icon} background={localeData.icon_background} /> diff --git a/web/app/components/app/configuration/tools/index.tsx b/web/app/components/app/configuration/tools/index.tsx index 2ec8f554413234..8c6bc6e5bff6c0 100644 --- a/web/app/components/app/configuration/tools/index.tsx +++ b/web/app/components/app/configuration/tools/index.tsx @@ -139,6 +139,7 @@ const Tools = () => {
diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index d2b7c0dabe2ce1..4061ec652f3a23 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -64,13 +64,12 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => { return isCreatingRef.current = true try { - if (appIcon.type === 'image') - throw new Error('unimplemented') const app = await createApp({ name, description, - icon: appIcon.icon, - icon_background: appIcon.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') }) @@ -274,8 +273,9 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
{ setShowAppIconPicker(true) }} /> diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index 6595972de615da..b1f70e16fd99f3 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -1,32 +1,40 @@ 'use client' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' +import type { AppIconSelection } from '../../base/app-icon-picker' +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_url: string show: boolean onConfirm: (info: { name: string + icon_type: AppIconType icon: string - icon_background: string + icon_background?: string }) => Promise onHide: () => void } const DuplicateAppModal = ({ appName, + icon_type, icon, icon_background, + icon_url, show = false, onConfirm, onHide, @@ -35,8 +43,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', url: icon_url, fileId: icon } + : { type: 'emoji', icon, background: icon_background }, + ) const { plan, enableBilling } = useProviderContext() const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) @@ -48,7 +60,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 +79,15 @@ const DuplicateAppModal = ({
{t('explore.appCustomize.subTitle')}
- { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> + { 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} + /> setName(e.target.value)} @@ -79,14 +101,16 @@ const DuplicateAppModal = ({
- {showEmojiPicker && { - setEmoji({ icon, icon_background }) - setShowEmojiPicker(false) + {showAppIconPicker && { + 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) }} />} diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 9f7b65da27847a..9d45ef20be0cbb 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -12,9 +12,10 @@ init({ data }) export type AppIconProps = { size?: 'xs' | 'tiny' | 'small' | 'medium' | 'large' rounded?: boolean - iconType?: AppIconType + iconType?: AppIconType | null icon?: string background?: string + imageUrl?: string className?: string innerIcon?: React.ReactNode onClick?: () => void @@ -23,9 +24,10 @@ export type AppIconProps = { const AppIcon: FC = ({ size = 'medium', rounded = false, - iconType = 'emoji', + iconType, icon, background, + imageUrl, className, innerIcon, onClick, @@ -38,10 +40,16 @@ const AppIcon: FC = ({ 'overflow-hidden', ) - return - {iconType === 'emoji' - ? (innerIcon || ((icon && icon !== '') ? : )) - : app icon + const isValidImageIcon = iconType === 'image' && imageUrl + + return + {isValidImageIcon + ? app icon + : (innerIcon || ((icon && icon !== '') ? : )) } } diff --git a/web/app/components/explore/app-card/index.tsx b/web/app/components/explore/app-card/index.tsx index 51c1ca6ce9a182..3d666fdb1a7dfb 100644 --- a/web/app/components/explore/app-card/index.tsx +++ b/web/app/components/explore/app-card/index.tsx @@ -26,7 +26,13 @@ const AppCard = ({
- + {appBasicInfo.mode === 'advanced-chat' && ( diff --git a/web/app/components/explore/sidebar/app-nav-item/index.tsx b/web/app/components/explore/sidebar/app-nav-item/index.tsx index 2a7f3342ab7e2b..73bd93585b6250 100644 --- a/web/app/components/explore/sidebar/app-nav-item/index.tsx +++ b/web/app/components/explore/sidebar/app-nav-item/index.tsx @@ -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 && } + {isMobile && } {!isMobile && ( <>
- +
{name}
e.stopPropagation()}> diff --git a/web/app/components/explore/sidebar/index.tsx b/web/app/components/explore/sidebar/index.tsx index 1a3e767baaa68a..a4a40a00a20e5f 100644 --- a/web/app/components/explore/sidebar/index.tsx +++ b/web/app/components/explore/sidebar/index.tsx @@ -109,14 +109,16 @@ const SideBar: FC = ({ 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 ( }> = ({ url, params }) => { @@ -21,16 +21,16 @@ export const fetchAppTemplates: Fetcher = return get(url) } -export const createApp: Fetcher = ({ name, icon, icon_background, mode, description, config }) => { - return post('apps', { body: { name, icon, icon_background, mode, description, model_config: config } }) +export const createApp: Fetcher = ({ name, icon_type, icon, icon_background, mode, description, config }) => { + return post('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } }) } export const updateAppInfo: Fetcher = ({ appID, name, icon, icon_background, description }) => { return put(`apps/${appID}`, { body: { name, icon, icon_background, description } }) } -export const copyApp: Fetcher = ({ appID, name, icon, icon_background, mode, description }) => { - return post(`apps/${appID}/copy`, { body: { name, icon, icon_background, mode, description } }) +export const copyApp: Fetcher = ({ appID, name, icon_type, icon, icon_background, mode, description }) => { + return post(`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 }) => { diff --git a/web/types/app.ts b/web/types/app.ts index ed74182793cbad..519c5e6ed6caf0 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -311,11 +311,13 @@ export type App = { description: string /** Icon Type */ - iconType: AppIconType - /** Icon */ + icon_type: AppIconType | null + /** Icon, stores file ID if icon_type is 'image' */ icon: string - /** Icon Background */ + /** Icon Background, only available when icon_type is null or 'emoji' */ icon_background: string + /** Icon URL, only available when icon_type is 'image' */ + icon_url: string /** Mode */ mode: AppMode From 286f214fee70196df401caffee93628dae95f69f Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Wed, 14 Aug 2024 14:06:00 +0800 Subject: [PATCH 07/16] chore: emoji-picker-inner --- .../components/base/app-icon-picker/index.tsx | 4 +- .../components/base/emoji-picker/Inner.tsx | 169 +++++++++++++++ .../components/base/emoji-picker/index.tsx | 194 ++++-------------- .../explore/create-app-modal/index.tsx | 1 - 4 files changed, 214 insertions(+), 154 deletions(-) create mode 100644 web/app/components/base/emoji-picker/Inner.tsx diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index f02e260ce45c1d..f7e7df1d622dcf 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -6,8 +6,8 @@ import Modal from '../modal' import Divider from '../divider' import Button from '../button' import { ImagePlus } from '../icons/src/vender/line/images' -import EmojiPicker from '../emoji-picker' 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' @@ -120,7 +120,7 @@ const AppIconPicker: FC = ({ - + diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx new file mode 100644 index 00000000000000..cf7cd39e047c35 --- /dev/null +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -0,0 +1,169 @@ +'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 > + } + } +} + +init({ data }) + +const backgroundColors = [ + '#FFEAD5', + '#E4FBCC', + '#D3F8DF', + '#E0F2FE', + + '#E0EAFF', + '#EFF1F5', + '#FBE8FF', + '#FCE7F6', + + '#FEF7C3', + '#E6F4D7', + '#D5F5F6', + '#D1E9FF', + + '#D1E0FF', + '#D5D9EB', + '#ECE9FE', + '#FFE4E8', +] + +type IEmojiPickerInnerProps = { + onSelect?: (emoji: string, background: string) => void + className?: string +} + +const EmojiPickerInner: FC = ({ + onSelect, + className, +}) => { + const { categories } = data as EmojiMartData + const [selectedEmoji, setSelectedEmoji] = useState('') + const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0]) + + const [searchedEmojis, setSearchedEmojis] = useState([]) + const [isSearching, setIsSearching] = useState(false) + + React.useEffect(() => { + if (selectedEmoji && selectedBackground) + onSelect?.(selectedEmoji, selectedBackground) + }, [onSelect, selectedEmoji, selectedBackground]) + + return
+
+
+
+
+ ) => { + if (e.target.value === '') { + setIsSearching(false) + } + else { + setIsSearching(true) + const emojis = await searchEmoji(e.target.value) + setSearchedEmojis(emojis) + } + }} + /> +
+
+ + +
+ {isSearching && <> +
+

Search

+
+ {searchedEmojis.map((emoji: string, index: number) => { + return
{ + setSelectedEmoji(emoji) + }} + > +
+ +
+
+ })} +
+
+ } + + {categories.map((category, index: number) => { + return
+

{category.id}

+
+ {category.emojis.map((emoji, index: number) => { + return
{ + setSelectedEmoji(emoji) + }} + > +
+ +
+
+ })} + +
+
+ })} +
+ + {/* Color Select */} +
+

Choose Style

+
+ {backgroundColors.map((color) => { + return
{ + setSelectedBackground(color) + }} + > +
+ {selectedEmoji !== '' && } +
+
+ })} +
+
+
+} +export default EmojiPickerInner diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index 02aa1ea5769caf..bfd625e04378b6 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -1,169 +1,61 @@ 'use client' -import type { ChangeEvent, FC } from 'react' +import type { 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 { 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 { 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 > - } - } -} - -init({ data }) - -const backgroundColors = [ - '#FFEAD5', - '#E4FBCC', - '#D3F8DF', - '#E0F2FE', - - '#E0EAFF', - '#EFF1F5', - '#FBE8FF', - '#FCE7F6', - - '#FEF7C3', - '#E6F4D7', - '#D5F5F6', - '#D1E9FF', - - '#D1E0FF', - '#D5D9EB', - '#ECE9FE', - '#FFE4E8', -] +import Button from '@/app/components/base/button' +import Modal from '@/app/components/base/modal' type IEmojiPickerProps = { + isModal?: boolean onSelect?: (emoji: string, background: string) => void + onClose?: () => void className?: string } const EmojiPicker: FC = ({ + isModal = true, onSelect, + onClose, className, }) => { - const { categories } = data as EmojiMartData + const { t } = useTranslation() const [selectedEmoji, setSelectedEmoji] = useState('') - const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0]) - - const [searchedEmojis, setSearchedEmojis] = useState([]) - const [isSearching, setIsSearching] = useState(false) - - React.useEffect(() => { - if (selectedEmoji && selectedBackground) - onSelect?.(selectedEmoji, selectedBackground) - }, [onSelect, selectedEmoji, selectedBackground]) - - return
-
-
-
-
- ) => { - if (e.target.value === '') { - setIsSearching(false) - } - else { - setIsSearching(true) - const emojis = await searchEmoji(e.target.value) - setSearchedEmojis(emojis) - } - }} - /> -
-
- - -
- {isSearching && <> -
-

Search

-
- {searchedEmojis.map((emoji: string, index: number) => { - return
{ - setSelectedEmoji(emoji) - }} - > -
- -
-
- })} -
-
- } - - {categories.map((category, index: number) => { - return
-

{category.id}

-
- {category.emojis.map((emoji, index: number) => { - return
{ - setSelectedEmoji(emoji) - }} - > -
- -
-
- })} - -
-
- })} -
- - {/* Color Select */} -
-

Choose Style

-
- {backgroundColors.map((color) => { - return
{ - setSelectedBackground(color) - }} - > -
- {selectedEmoji !== '' && } -
-
- })} + const [selectedBackground, setSelectedBackground] = useState() + + return isModal + ? { }} + isShow + closable={false} + wrapperClassName={className} + className={cn(s.container, '!w-[362px] !p-0')} + > + { + setSelectedEmoji(emoji) + setSelectedBackground(background) + }} /> + +
+ +
-
-
+ + : <> } export default EmojiPicker diff --git a/web/app/components/explore/create-app-modal/index.tsx b/web/app/components/explore/create-app-modal/index.tsx index 714123c5722d39..b9343f8bcfa755 100644 --- a/web/app/components/explore/create-app-modal/index.tsx +++ b/web/app/components/explore/create-app-modal/index.tsx @@ -117,7 +117,6 @@ const CreateAppModal = ({ }} />} - ) } From d61fd0e73933d9cb68585d0b2b60d3c28a32d837 Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Wed, 14 Aug 2024 14:11:01 +0800 Subject: [PATCH 08/16] chore: remove unused function --- web/service/apps.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/service/apps.ts b/web/service/apps.ts index de7dc4912ffb0e..d0dec0c440cdda 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -9,10 +9,6 @@ export const fetchAppList: Fetcher(url, { params }) } -export const fetchAppIconPreviewUrl = ({ fileID }: { fileID: string }) => { - return get(`/apps/icon-preview-url/${fileID}`) -} - export const fetchAppDetail = ({ url, id }: { url: string; id: string }) => { return get(`${url}/${id}`) } From 037ab6d0502bbc4a985d282470df8fb7cce6a93b Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Wed, 14 Aug 2024 14:17:44 +0800 Subject: [PATCH 09/16] style: minor adjust --- web/app/components/base/app-icon-picker/Uploader.tsx | 4 ++-- web/app/components/base/emoji-picker/Inner.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/app/components/base/app-icon-picker/Uploader.tsx b/web/app/components/base/app-icon-picker/Uploader.tsx index eb6e212b6a67fc..4ddaa404475e6d 100644 --- a/web/app/components/base/app-icon-picker/Uploader.tsx +++ b/web/app/components/base/app-icon-picker/Uploader.tsx @@ -53,9 +53,9 @@ const Uploader: FC = ({ const inputRef = createRef() return ( -
+
= ({ }, [onSelect, selectedEmoji, selectedBackground]) return
-
+
{/* Color Select */} -
+

Choose Style

{backgroundColors.map((color) => { From 8621df3758eacff61a58c58ce1e10b470c1918e1 Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Thu, 15 Aug 2024 15:32:48 +0800 Subject: [PATCH 10/16] chore: refine --- api/controllers/console/app/app.py | 2 + api/controllers/console/app/site.py | 2 + api/controllers/console/app/workflow.py | 1 + api/controllers/web/site.py | 3 + .../create_site_record_when_app_created.py | 1 + api/fields/app_fields.py | 3 +- api/services/app_dsl_service.py | 4 ++ api/services/app_service.py | 1 + api/services/workflow/workflow_converter.py | 3 + api/services/workflow_service.py | 1 + web/app/(commonLayout)/apps/AppCard.tsx | 6 +- web/app/components/app-sidebar/app-info.tsx | 6 +- .../tools/external-data-tool-modal.tsx | 1 - .../app/configuration/tools/index.tsx | 1 - .../components/app/duplicate-modal/index.tsx | 17 +++--- .../app/overview/settings/index.tsx | 55 ++++++++++++------- .../components/app/switch-app-modal/index.tsx | 40 ++++++++++---- .../components/base/app-icon-picker/index.tsx | 10 ++-- web/app/components/base/app-icon/index.tsx | 4 +- .../base/chat/chat-with-history/hooks.tsx | 10 +++- .../chat/chat-with-history/sidebar/index.tsx | 2 + .../components/base/emoji-picker/Inner.tsx | 2 + .../components/base/emoji-picker/index.tsx | 14 +++-- web/app/components/explore/app-list/index.tsx | 4 ++ .../explore/create-app-modal/index.tsx | 50 ++++++++++++----- .../share/text-generation/index.tsx | 8 ++- web/hooks/use-app-favicon.ts | 35 +++++++++--- web/models/share.ts | 3 + web/service/apps.ts | 14 ++--- web/types/app.ts | 13 +++-- 30 files changed, 224 insertions(+), 92 deletions(-) diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index 60925ce1855950..8651597fd700a3 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -95,6 +95,7 @@ def post(self): 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() @@ -168,6 +169,7 @@ def put(self, app_model): 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') diff --git a/api/controllers/console/app/site.py b/api/controllers/console/app/site.py index 6aa9f0b475f161..7db58c048abcb0 100644 --- a/api/controllers/console/app/site.py +++ b/api/controllers/console/app/site.py @@ -16,6 +16,7 @@ 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 @@ def post(self, app_model): for attr_name in [ 'title', + 'icon_type', 'icon', 'icon_background', 'description', diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 686ef7b4bebaaa..8889d6c6002057 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -454,6 +454,7 @@ def post(self, app_model: App): 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() diff --git a/api/controllers/web/site.py b/api/controllers/web/site.py index 99ec86e935e333..0f4a7cabe5d900 100644 --- a/api/controllers/web/site.py +++ b/api/controllers/web/site.py @@ -6,6 +6,7 @@ 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, diff --git a/api/events/event_handlers/create_site_record_when_app_created.py b/api/events/event_handlers/create_site_record_when_app_created.py index f0eb7159b631e0..a3dcda61388826 100644 --- a/api/events/event_handlers/create_site_record_when_app_created.py +++ b/api/events/event_handlers/create_site_record_when_app_created.py @@ -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, diff --git a/api/fields/app_fields.py b/api/fields/app_fields.py index 6c5cd4f386031a..a550a161373e73 100644 --- a/api/fields/app_fields.py +++ b/api/fields/app_fields.py @@ -112,9 +112,10 @@ 'access_token': fields.String(attribute='code'), 'code': fields.String, 'title': fields.String, - 'icon': 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, diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 040a71d1f3a968..af0da2e87ec525 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -210,6 +210,7 @@ def _import_and_create_new_workflow_based_app(cls, account: Account, name: str, description: str, + icon_type: str, icon: str, icon_background: str) -> App: """ @@ -221,6 +222,7 @@ def _import_and_create_new_workflow_based_app(cls, :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 """ @@ -234,6 +236,7 @@ def _import_and_create_new_workflow_based_app(cls, account=account, name=name, description=description, + icon_type=icon_type, icon=icon, icon_background=icon_background ) @@ -368,6 +371,7 @@ def _create_app(cls, :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 """ diff --git a/api/services/app_service.py b/api/services/app_service.py index c084c30fcc4bfa..f33ef9b0013580 100644 --- a/api/services/app_service.py +++ b/api/services/app_service.py @@ -211,6 +211,7 @@ def update_app(self, app: App, args: dict) -> App: 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) diff --git a/api/services/workflow/workflow_converter.py b/api/services/workflow/workflow_converter.py index 06b129be691010..74c32f5097314d 100644 --- a/api/services/workflow/workflow_converter.py +++ b/api/services/workflow/workflow_converter.py @@ -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 @@ def convert_to_workflow(self, app_model: App, :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 @@ def convert_to_workflow(self, app_model: App, 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 diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 38de538a19fd18..6101ead1d5470b 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -298,6 +298,7 @@ def convert_to_workflow(self, app_model: App, account: Account, args: dict) -> A 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'), ) diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index d7fd96869e7882..bc7308a711a06b 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -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, @@ -363,9 +365,11 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { {showEditModal && ( { 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, @@ -415,9 +417,11 @@ const AppInfo = ({ expand }: IAppInfoProps) => { {showEditModal && ( = ({ { setShowEmojiPicker(true) }} className='!w-9 !h-9 rounded-lg border-[0.5px] border-black/5 cursor-pointer ' - iconType="emoji" icon={localeData.icon} background={localeData.icon_background} /> diff --git a/web/app/components/app/configuration/tools/index.tsx b/web/app/components/app/configuration/tools/index.tsx index 8c6bc6e5bff6c0..2ec8f554413234 100644 --- a/web/app/components/app/configuration/tools/index.tsx +++ b/web/app/components/app/configuration/tools/index.tsx @@ -139,7 +139,6 @@ const Tools = () => {
diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index b1f70e16fd99f3..59a53101dbed2d 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -1,7 +1,6 @@ 'use client' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import type { AppIconSelection } from '../../base/app-icon-picker' import AppIconPicker from '../../base/app-icon-picker' import s from './style.module.css' import cn from '@/utils/classnames' @@ -17,14 +16,14 @@ export type DuplicateAppModalProps = { appName: string icon_type: AppIconType | null icon: string - icon_background: string - icon_url: 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 onHide: () => void } @@ -44,10 +43,10 @@ const DuplicateAppModal = ({ const [name, setName] = React.useState(appName) const [showAppIconPicker, setShowAppIconPicker] = useState(false) - const [appIcon, setAppIcon] = useState( + const [appIcon, setAppIcon] = useState( icon_type === 'image' - ? { type: 'image', url: icon_url, fileId: icon } - : { type: 'emoji', icon, background: icon_background }, + ? { type: 'image' as const, url: icon_url, fileId: icon } + : { type: 'emoji' as const, icon, background: icon_background }, ) const { plan, enableBilling } = useProviderContext() @@ -108,8 +107,8 @@ const DuplicateAppModal = ({ }} onClose={() => { setAppIcon(icon_type === 'image' - ? { type: 'image', url: icon_url, fileId: icon } - : { type: 'emoji', icon, background: icon_background }) + ? { type: 'image', url: icon_url!, fileId: icon } + : { type: 'emoji', icon, background: icon_background! }) setShowAppIconPicker(false) }} />} diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 88d5c2d9094585..8da9b9864f5046 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -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 = ({ }) => { 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 = ({ 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( + 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 = ({ 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 = ({ 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 = ({
{t(`${prefixSettings}.webName`)}
{ 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} /> = ({
- {showEmojiPicker && { - setEmoji({ icon, icon_background }) - setShowEmojiPicker(false) + {showAppIconPicker && { + 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) }} />} diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index 79f4811e433f0a..82f57e1f5a107c 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -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(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
{t('app.switchLabel')}
- { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> + { 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} + /> 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' />
- {showEmojiPicker && { - setEmoji({ icon, icon_background }) - setShowEmojiPicker(false) + {showAppIconPicker && { + 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) }} />}
diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index f7e7df1d622dcf..a618c34f592a9d 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import { useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import type { Area } from 'react-easy-crop' import Modal from '../modal' @@ -14,13 +14,13 @@ import getCroppedImg from './utils' import type { AppIconType, ImageFile } from '@/types/app' import cn from '@/utils/classnames' -type AppIconEmojiSelection = { +export type AppIconEmojiSelection = { type: 'emoji' icon: string background: string } -type AppIconImageSelection = { +export type AppIconImageSelection = { type: 'image' fileId: string url: string @@ -48,9 +48,9 @@ const AppIconPicker: FC = ({ const [activeTab, setActiveTab] = useState('emoji') const [emoji, setEmoji] = useState<{ emoji: string; background: string }>() - const handleSelectEmoji = (emoji: string, background: string) => { + const handleSelectEmoji = useCallback((emoji: string, background: string) => { setEmoji({ emoji, background }) - } + }, [setEmoji]) const [uploading, setUploading] = useState() diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 9d45ef20be0cbb..5e7378c08734aa 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -14,8 +14,8 @@ export type AppIconProps = { rounded?: boolean iconType?: AppIconType | null icon?: string - background?: string - imageUrl?: string + background?: string | null + imageUrl?: string | null className?: string innerIcon?: React.ReactNode onClick?: () => void diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 3fa301d268744d..624cc53a183609 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -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, diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index dd145c36b9c119..91ceb8b5126453 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -67,8 +67,10 @@ const Sidebar = () => {
{appData?.site.title} diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index 30135a42f5c2dd..36c146a2a0c979 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -45,6 +45,8 @@ const backgroundColors = [ ] type IEmojiPickerInnerProps = { + emoji?: string + background?: string onSelect?: (emoji: string, background: string) => void className?: string } diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index bfd625e04378b6..3add14879aa5bb 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useState } from 'react' +import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import s from './style.module.css' import EmojiPickerInner from './Inner' @@ -26,6 +26,11 @@ const EmojiPicker: FC = ({ const [selectedEmoji, setSelectedEmoji] = useState('') const [selectedBackground, setSelectedBackground] = useState() + const handleSelectEmoji = useCallback((emoji: string, background: string) => { + setSelectedEmoji(emoji) + setSelectedBackground(background) + }, [setSelectedEmoji, setSelectedBackground]) + return isModal ? { }} @@ -34,10 +39,9 @@ const EmojiPicker: FC = ({ wrapperClassName={className} className={cn(s.container, '!w-[362px] !p-0')} > - { - setSelectedEmoji(emoji) - setSelectedBackground(background) - }} /> +
{isShowCreateModal && ( Promise 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 = ({
{t('app.newApp.captionName')}
- { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> + { 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} + /> setName(e.target.value)} @@ -106,14 +126,16 @@ const CreateAppModal = ({
- {showEmojiPicker && { - setEmoji({ icon, icon_background }) - setShowEmojiPicker(false) + {showAppIconPicker && { + 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) }} />} diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 1bc41a54400ebf..2cb942ca260964 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -411,7 +411,13 @@ const TextGeneration: FC = ({ } }, [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 = () => { diff --git a/web/hooks/use-app-favicon.ts b/web/hooks/use-app-favicon.ts index 8904b884ced2f4..86eadc1b3d0862 100644 --- a/web/hooks/use-app-favicon.ts +++ b/web/hooks/use-app-favicon.ts @@ -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,' - + '' - + '' - + (icon ? await searchEmoji(icon) : '🤖') - + '' + link.href = isValidImageIcon + ? icon_url + : 'data:image/svg+xml,' + + `` + + `${ + icon ? await searchEmoji(icon) : '🤖' + }` + '' link.rel = 'shortcut icon' diff --git a/web/models/share.ts b/web/models/share.ts index 47a87e2fdb67e4..24b355a71b0a45 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -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 diff --git a/web/service/apps.ts b/web/service/apps.ts index d0dec0c440cdda..7049af82cf5f79 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -21,11 +21,11 @@ export const createApp: Fetcher('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } }) } -export const updateAppInfo: Fetcher = ({ appID, name, icon, icon_background, description }) => { - return put(`apps/${appID}`, { body: { name, icon, icon_background, description } }) +export const updateAppInfo: Fetcher = ({ appID, name, icon_type, icon, icon_background, description }) => { + return put(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description } }) } -export const copyApp: Fetcher = ({ appID, name, icon_type, icon, icon_background, mode, description }) => { +export const copyApp: Fetcher = ({ appID, name, icon_type, icon, icon_background, mode, description }) => { return post(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } }) } @@ -33,16 +33,16 @@ export const exportAppConfig: Fetcher<{ data: string }, { appID: string; include return get<{ data: string }>(`apps/${appID}/export?include_secret=${include}`) } -export const importApp: Fetcher = ({ data, name, description, icon, icon_background }) => { - return post('apps/import', { body: { data, name, description, icon, icon_background } }) +export const importApp: Fetcher = ({ data, name, description, icon_type, icon, icon_background }) => { + return post('apps/import', { body: { data, name, description, icon_type, icon, icon_background } }) } export const importAppFromUrl: Fetcher = ({ url, name, description, icon, icon_background }) => { return post('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 = (appID) => { diff --git a/web/types/app.ts b/web/types/app.ts index 519c5e6ed6caf0..ed3c24234d8c0e 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -291,8 +291,10 @@ 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 } @@ -310,14 +312,17 @@ export type App = { /** Description */ description: string - /** Icon Type */ + /** + * Icon Type + * @default 'emoji' + */ icon_type: AppIconType | null /** Icon, stores file ID if icon_type is 'image' */ icon: string /** Icon Background, only available when icon_type is null or 'emoji' */ - icon_background: string + icon_background: string | null /** Icon URL, only available when icon_type is 'image' */ - icon_url: string + icon_url: string | null /** Mode */ mode: AppMode From ae78ed19c14fb10bf4965c3a5d84d28afc43e187 Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Thu, 15 Aug 2024 18:06:36 +0800 Subject: [PATCH 11/16] chore: resolve migration conflict --- ...8_15_1001-a6be81136580_app_and_site_icon_type.py} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename api/migrations/versions/{2024_08_09_0315-44d0f8e91926_add_icon_type.py => 2024_08_15_1001-a6be81136580_app_and_site_icon_type.py} (84%) diff --git a/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py b/api/migrations/versions/2024_08_15_1001-a6be81136580_app_and_site_icon_type.py similarity index 84% rename from api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py rename to api/migrations/versions/2024_08_15_1001-a6be81136580_app_and_site_icon_type.py index 2b6b452ab27e49..d814666eefd2f2 100644 --- a/api/migrations/versions/2024_08_09_0315-44d0f8e91926_add_icon_type.py +++ b/api/migrations/versions/2024_08_15_1001-a6be81136580_app_and_site_icon_type.py @@ -1,8 +1,8 @@ -"""add icon_type +"""app and site icon type -Revision ID: 44d0f8e91926 -Revises: eeb2e349e6ac -Create Date: 2024-08-09 03:15:39.557824 +Revision ID: a6be81136580 +Revises: 8782057ff0dc +Create Date: 2024-08-15 10:01:24.697888 """ import sqlalchemy as sa @@ -11,8 +11,8 @@ import models as models # revision identifiers, used by Alembic. -revision = '44d0f8e91926' -down_revision = 'eeb2e349e6ac' +revision = 'a6be81136580' +down_revision = '8782057ff0dc' branch_labels = None depends_on = None From 877be19ddebdc1a06e9bb12a6923f47dee4505a1 Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Fri, 16 Aug 2024 10:51:13 +0800 Subject: [PATCH 12/16] chore: lint python --- api/libs/helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/libs/helper.py b/api/libs/helper.py index 39d1184540d72f..6802aec8a1725f 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -24,14 +24,16 @@ 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 - if obj.icon_type == 'image': + if obj.icon_type == "image": return UploadFileParser.get_signed_temp_image_url(obj.icon) return None + class TimestampField(fields.Raw): def format(self, value) -> int: return int(value.timestamp()) From 03150fdc297d862dbef9bfacf8a9373410539202 Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Fri, 16 Aug 2024 11:37:06 +0800 Subject: [PATCH 13/16] fix: use emoji icon when exporting app as DSL --- api/services/app_dsl_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 226f311d242e99..737def3366fdba 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -168,8 +168,8 @@ def export_dsl(cls, app_model: App, include_secret:bool = False) -> str: "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 } } From abd90935b812d3afa166f85895882fc29c975d6b Mon Sep 17 00:00:00 2001 From: xuzuodong Date: Fri, 16 Aug 2024 11:37:50 +0800 Subject: [PATCH 14/16] chore: code style --- .../components/base/chat/chat-with-history/sidebar/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx index 91ceb8b5126453..17a2751f111928 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/index.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/index.tsx @@ -67,7 +67,7 @@ const Sidebar = () => { Date: Fri, 16 Aug 2024 15:49:08 +0800 Subject: [PATCH 15/16] refactor: update AppIconUrlField to use IconType enum --- api/libs/helper.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/libs/helper.py b/api/libs/helper.py index 6802aec8a1725f..af0c2dace1a5dd 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -29,7 +29,10 @@ class AppIconUrlField(fields.Raw): def output(self, key, obj): if obj is None: return None - if obj.icon_type == "image": + + from models.model import IconType + + if obj.icon_type == IconType.IMAGE.value: return UploadFileParser.get_signed_temp_image_url(obj.icon) return None From c1cfe1cf71e33d950c5e336bfbdb13de9de79c2d Mon Sep 17 00:00:00 2001 From: crazywoola <427733928@qq.com> Date: Fri, 16 Aug 2024 16:42:54 +0800 Subject: [PATCH 16/16] feat: add env settings --- web/.env.example | 5 ++++- web/app/components/base/app-icon-picker/index.tsx | 6 +++--- web/config/index.ts | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/web/.env.example b/web/.env.example index 439092c20e0a0e..3045cef2f9d258 100644 --- a/web/.env.example +++ b/web/.env.example @@ -15,4 +15,7 @@ NEXT_PUBLIC_PUBLIC_API_PREFIX=http://localhost:5001/api NEXT_PUBLIC_SENTRY_DSN= # Disable Next.js Telemetry (https://nextjs.org/telemetry) -NEXT_TELEMETRY_DISABLED=1 \ No newline at end of file +NEXT_TELEMETRY_DISABLED=1 + +# Disable Upload Image as WebApp icon default is false +NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index a618c34f592a9d..825481547501a8 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -13,7 +13,7 @@ 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 @@ -101,7 +101,7 @@ const AppIconPicker: FC = ({ wrapperClassName={className} className={cn(s.container, '!w-[362px] !p-0')} > -
+ {!DISABLE_UPLOAD_IMAGE_AS_ICON &&
{tabs.map(tab => ( ))}
-
+
} diff --git a/web/config/index.ts b/web/config/index.ts index 6b0e551e35c294..bb100473b1451c 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -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'