Browse Source

feat: allow users to use the app icon as the answer icon (#7888)

Co-authored-by: crazywoola <427733928@qq.com>
kurokobo 7 months ago
parent
commit
80aa7c4019

+ 1 - 0
api/controllers/console/app/app.py

@@ -174,6 +174,7 @@ class AppApi(Resource):
         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")
+        parser.add_argument("use_icon_as_answer_icon", type=bool, location="json")
         args = parser.parse_args()
 
         app_service = AppService()

+ 2 - 0
api/controllers/console/app/site.py

@@ -34,6 +34,7 @@ def parse_app_site_args():
     )
     parser.add_argument("prompt_public", type=bool, required=False, location="json")
     parser.add_argument("show_workflow_steps", type=bool, required=False, location="json")
+    parser.add_argument("use_icon_as_answer_icon", type=bool, required=False, location="json")
     return parser.parse_args()
 
 
@@ -68,6 +69,7 @@ class AppSite(Resource):
             "customize_token_strategy",
             "prompt_public",
             "show_workflow_steps",
+            "use_icon_as_answer_icon",
         ]:
             value = args.get(attr_name)
             if value is not None:

+ 1 - 0
api/controllers/web/site.py

@@ -39,6 +39,7 @@ class AppSiteApi(WebApiResource):
         "default_language": fields.String,
         "prompt_public": fields.Boolean,
         "show_workflow_steps": fields.Boolean,
+        "use_icon_as_answer_icon": fields.Boolean,
     }
 
     app_fields = {

+ 5 - 0
api/fields/app_fields.py

@@ -58,6 +58,7 @@ app_detail_fields = {
     "model_config": fields.Nested(model_config_fields, attribute="app_model_config", allow_null=True),
     "workflow": fields.Nested(workflow_partial_fields, allow_null=True),
     "tracing": fields.Raw,
+    "use_icon_as_answer_icon": fields.Boolean,
     "created_by": fields.String,
     "created_at": TimestampField,
     "updated_by": fields.String,
@@ -91,6 +92,7 @@ app_partial_fields = {
     "icon_url": AppIconUrlField,
     "model_config": fields.Nested(model_config_partial_fields, attribute="app_model_config", allow_null=True),
     "workflow": fields.Nested(workflow_partial_fields, allow_null=True),
+    "use_icon_as_answer_icon": fields.Boolean,
     "created_by": fields.String,
     "created_at": TimestampField,
     "updated_by": fields.String,
@@ -140,6 +142,7 @@ site_fields = {
     "prompt_public": fields.Boolean,
     "app_base_url": fields.String,
     "show_workflow_steps": fields.Boolean,
+    "use_icon_as_answer_icon": fields.Boolean,
     "created_by": fields.String,
     "created_at": TimestampField,
     "updated_by": fields.String,
@@ -161,6 +164,7 @@ app_detail_fields_with_site = {
     "workflow": fields.Nested(workflow_partial_fields, allow_null=True),
     "site": fields.Nested(site_fields),
     "api_base_url": fields.String,
+    "use_icon_as_answer_icon": fields.Boolean,
     "created_by": fields.String,
     "created_at": TimestampField,
     "updated_by": fields.String,
@@ -184,4 +188,5 @@ app_site_fields = {
     "customize_token_strategy": fields.String,
     "prompt_public": fields.Boolean,
     "show_workflow_steps": fields.Boolean,
+    "use_icon_as_answer_icon": fields.Boolean,
 }

+ 1 - 0
api/fields/installed_app_fields.py

@@ -10,6 +10,7 @@ app_fields = {
     "icon": fields.String,
     "icon_background": fields.String,
     "icon_url": AppIconUrlField,
+    "use_icon_as_answer_icon": fields.Boolean,
 }
 
 installed_app_fields = {

+ 45 - 0
api/migrations/versions/2024_09_01_1255-030f4915f36a_add_use_icon_as_answer_icon_fields_for_.py

@@ -0,0 +1,45 @@
+"""add use_icon_as_answer_icon fields for app and site
+
+Revision ID: 030f4915f36a
+Revises: d0187d6a88dd
+Create Date: 2024-09-01 12:55:45.129687
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+import models as models
+
+# revision identifiers, used by Alembic.
+revision = "030f4915f36a"
+down_revision = "d0187d6a88dd"
+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("use_icon_as_answer_icon", sa.Boolean(), server_default=sa.text("false"), nullable=False)
+        )
+
+    with op.batch_alter_table("sites", schema=None) as batch_op:
+        batch_op.add_column(
+            sa.Column("use_icon_as_answer_icon", sa.Boolean(), server_default=sa.text("false"), nullable=False)
+        )
+
+    # ### 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("use_icon_as_answer_icon")
+
+    with op.batch_alter_table("apps", schema=None) as batch_op:
+        batch_op.drop_column("use_icon_as_answer_icon")
+
+    # ### end Alembic commands ###

+ 2 - 0
api/models/model.py

@@ -86,6 +86,7 @@ class App(db.Model):
     created_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
     updated_by = db.Column(StringUUID, nullable=True)
     updated_at = db.Column(db.DateTime, nullable=False, server_default=db.text('CURRENT_TIMESTAMP(0)'))
+    use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
 
     @property
     def desc_or_prompt(self):
@@ -1114,6 +1115,7 @@ class Site(db.Model):
     copyright = db.Column(db.String(255))
     privacy_policy = db.Column(db.String(255))
     show_workflow_steps = db.Column(db.Boolean, nullable=False, server_default=db.text('true'))
+    use_icon_as_answer_icon = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
     custom_disclaimer = db.Column(db.String(255), nullable=True)
     customize_domain = db.Column(db.String(255))
     customize_token_strategy = db.Column(db.String(255), nullable=False)

+ 12 - 0
api/services/app_dsl_service.py

@@ -87,6 +87,7 @@ class AppDslService:
         icon_background = (
             args.get("icon_background") if args.get("icon_background") else app_data.get("icon_background")
         )
+        use_icon_as_answer_icon = app_data.get("use_icon_as_answer_icon", False)
 
         # import dsl and create app
         app_mode = AppMode.value_of(app_data.get("mode"))
@@ -101,6 +102,7 @@ class AppDslService:
                 icon_type=icon_type,
                 icon=icon,
                 icon_background=icon_background,
+                use_icon_as_answer_icon=use_icon_as_answer_icon,
             )
         elif app_mode in [AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.COMPLETION]:
             app = cls._import_and_create_new_model_config_based_app(
@@ -113,6 +115,7 @@ class AppDslService:
                 icon_type=icon_type,
                 icon=icon,
                 icon_background=icon_background,
+                use_icon_as_answer_icon=use_icon_as_answer_icon,
             )
         else:
             raise ValueError("Invalid app mode")
@@ -171,6 +174,7 @@ class AppDslService:
                 "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,
+                "use_icon_as_answer_icon": app_model.use_icon_as_answer_icon,
             },
         }
 
@@ -218,6 +222,7 @@ class AppDslService:
         icon_type: str,
         icon: str,
         icon_background: str,
+        use_icon_as_answer_icon: bool,
     ) -> App:
         """
         Import app dsl and create new workflow based app
@@ -231,6 +236,7 @@ class AppDslService:
         :param icon_type: app icon type, "emoji" or "image"
         :param icon: app icon
         :param icon_background: app icon background
+        :param use_icon_as_answer_icon: use app icon as answer icon
         """
         if not workflow_data:
             raise ValueError("Missing workflow in data argument " "when app mode is advanced-chat or workflow")
@@ -244,6 +250,7 @@ class AppDslService:
             icon_type=icon_type,
             icon=icon,
             icon_background=icon_background,
+            use_icon_as_answer_icon=use_icon_as_answer_icon,
         )
 
         # init draft workflow
@@ -316,6 +323,7 @@ class AppDslService:
         icon_type: str,
         icon: str,
         icon_background: str,
+        use_icon_as_answer_icon: bool,
     ) -> App:
         """
         Import app dsl and create new model config based app
@@ -341,6 +349,7 @@ class AppDslService:
             icon_type=icon_type,
             icon=icon,
             icon_background=icon_background,
+            use_icon_as_answer_icon=use_icon_as_answer_icon,
         )
 
         app_model_config = AppModelConfig()
@@ -369,6 +378,7 @@ class AppDslService:
         icon_type: str,
         icon: str,
         icon_background: str,
+        use_icon_as_answer_icon: bool,
     ) -> App:
         """
         Create new app
@@ -381,6 +391,7 @@ class AppDslService:
         :param icon_type: app icon type, "emoji" or "image"
         :param icon: app icon
         :param icon_background: app icon background
+        :param use_icon_as_answer_icon: use app icon as answer icon
         """
         app = App(
             tenant_id=tenant_id,
@@ -392,6 +403,7 @@ class AppDslService:
             icon_background=icon_background,
             enable_site=True,
             enable_api=True,
+            use_icon_as_answer_icon=use_icon_as_answer_icon,
             created_by=account.id,
             updated_by=account.id,
         )

+ 1 - 0
api/services/app_service.py

@@ -221,6 +221,7 @@ class AppService:
         app.icon_type = args.get("icon_type", "emoji")
         app.icon = args.get("icon")
         app.icon_background = args.get("icon_background")
+        app.use_icon_as_answer_icon = args.get("use_icon_as_answer_icon", False)
         app.updated_by = current_user.id
         app.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
         db.session.commit()

+ 4 - 0
web/app/(commonLayout)/apps/AppCard.tsx

@@ -79,6 +79,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
     icon,
     icon_background,
     description,
+    use_icon_as_answer_icon,
   }) => {
     try {
       await updateAppInfo({
@@ -88,6 +89,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
         icon,
         icon_background,
         description,
+        use_icon_as_answer_icon,
       })
       setShowEditModal(false)
       notify({
@@ -370,6 +372,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
           appIconBackground={app.icon_background}
           appIconUrl={app.icon_url}
           appDescription={app.description}
+          appMode={app.mode}
+          appUseIconAsAnswerIcon={app.use_icon_as_answer_icon}
           show={showEditModal}
           onConfirm={onEdit}
           onHide={() => setShowEditModal(false)}

+ 4 - 0
web/app/components/app-sidebar/app-info.tsx

@@ -63,6 +63,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
     icon,
     icon_background,
     description,
+    use_icon_as_answer_icon,
   }) => {
     if (!appDetail)
       return
@@ -74,6 +75,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
         icon,
         icon_background,
         description,
+        use_icon_as_answer_icon,
       })
       setShowEditModal(false)
       notify({
@@ -423,6 +425,8 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
             appIconBackground={appDetail.icon_background}
             appIconUrl={appDetail.icon_url}
             appDescription={appDetail.description}
+            appMode={appDetail.mode}
+            appUseIconAsAnswerIcon={appDetail.use_icon_as_answer_icon}
             show={showEditModal}
             onConfirm={onEdit}
             onHide={() => setShowEditModal(false)}

+ 18 - 0
web/app/components/app/overview/settings/index.tsx

@@ -43,6 +43,7 @@ export type ConfigParams = {
   icon: string
   icon_background?: string
   show_workflow_steps: boolean
+  use_icon_as_answer_icon: boolean
   enable_sso?: boolean
 }
 
@@ -72,6 +73,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
     custom_disclaimer,
     default_language,
     show_workflow_steps,
+    use_icon_as_answer_icon,
   } = appInfo.site
   const [inputInfo, setInputInfo] = useState({
     title,
@@ -82,6 +84,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
     privacyPolicy: privacy_policy,
     customDisclaimer: custom_disclaimer,
     show_workflow_steps,
+    use_icon_as_answer_icon,
     enable_sso: appInfo.enable_sso,
   })
   const [language, setLanguage] = useState(default_language)
@@ -94,6 +97,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
       ? { type: 'image', url: icon_url!, fileId: icon }
       : { type: 'emoji', icon, background: icon_background! },
   )
+  const isChatBot = appInfo.mode === 'chat' || appInfo.mode === 'advanced-chat' || appInfo.mode === 'agent-chat'
 
   useEffect(() => {
     setInputInfo({
@@ -105,6 +109,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
       privacyPolicy: privacy_policy,
       customDisclaimer: custom_disclaimer,
       show_workflow_steps,
+      use_icon_as_answer_icon,
       enable_sso: appInfo.enable_sso,
     })
     setLanguage(default_language)
@@ -157,6 +162,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
       icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
       icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
       show_workflow_steps: inputInfo.show_workflow_steps,
+      use_icon_as_answer_icon: inputInfo.use_icon_as_answer_icon,
       enable_sso: inputInfo.enable_sso,
     }
     await onSave?.(params)
@@ -209,6 +215,18 @@ const SettingsModal: FC<ISettingsModalProps> = ({
           onChange={onChange('desc')}
           placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
         />
+        {isChatBot && (
+          <div className='w-full mt-4'>
+            <div className='flex justify-between items-center'>
+              <div className={`font-medium ${s.settingTitle} text-gray-900 `}>{t('app.answerIcon.title')}</div>
+              <Switch
+                defaultValue={inputInfo.use_icon_as_answer_icon}
+                onChange={v => setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })}
+              />
+            </div>
+            <p className='body-xs-regular text-gray-500'>{t('app.answerIcon.description')}</p>
+          </div>
+        )}
         <div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
         <SimpleSelect
           items={languages.filter(item => item.supported)}

+ 47 - 0
web/app/components/base/answer-icon/index.tsx

@@ -0,0 +1,47 @@
+'use client'
+
+import type { FC } from 'react'
+import { init } from 'emoji-mart'
+import data from '@emoji-mart/data'
+import classNames from '@/utils/classnames'
+import type { AppIconType } from '@/types/app'
+
+init({ data })
+
+export type AnswerIconProps = {
+  iconType?: AppIconType | null
+  icon?: string | null
+  background?: string | null
+  imageUrl?: string | null
+}
+
+const AnswerIcon: FC<AnswerIconProps> = ({
+  iconType,
+  icon,
+  background,
+  imageUrl,
+}) => {
+  const wrapperClassName = classNames(
+    'flex',
+    'items-center',
+    'justify-center',
+    'w-full',
+    'h-full',
+    'rounded-full',
+    'border-[0.5px]',
+    'border-black/5',
+    'text-xl',
+  )
+  const isValidImageIcon = iconType === 'image' && imageUrl
+  return <div
+    className={wrapperClassName}
+    style={{ background: background || '#D5F5F6' }}
+  >
+    {isValidImageIcon
+      ? <img src={imageUrl} className="w-full h-full rounded-full" alt="answer icon" />
+      : (icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />
+    }
+  </div>
+}
+
+export default AnswerIcon

+ 11 - 0
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx

@@ -13,6 +13,7 @@ import {
   getUrl,
   stopChatMessageResponding,
 } from '@/service/share'
+import AnswerIcon from '@/app/components/base/answer-icon'
 
 const ChatWrapper = () => {
   const {
@@ -128,6 +129,15 @@ const ChatWrapper = () => {
     isMobile,
   ])
 
+  const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
+    ? <AnswerIcon
+      iconType={appData.site.icon_type}
+      icon={appData.site.icon}
+      background={appData.site.icon_background}
+      imageUrl={appData.site.icon_url}
+    />
+    : null
+
   return (
     <Chat
       appData={appData}
@@ -143,6 +153,7 @@ const ChatWrapper = () => {
       allToolIcons={appMeta?.tool_icons || {}}
       onFeedback={handleFeedback}
       suggestedQuestions={suggestedQuestions}
+      answerIcon={answerIcon}
       hideProcessDetail
       themeBuilder={themeBuilder}
     />

+ 1 - 0
web/app/components/base/chat/chat-with-history/hooks.tsx

@@ -65,6 +65,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
           prompt_public: false,
           copyright: '',
           show_workflow_steps: true,
+          use_icon_as_answer_icon: app.use_icon_as_answer_icon,
         },
         plan: 'basic',
       } as AppData

+ 2 - 5
web/app/components/base/chat/chat/answer/index.tsx

@@ -22,6 +22,7 @@ import Citation from '@/app/components/base/chat/chat/citation'
 import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
 import type { Emoji } from '@/app/components/tools/types'
 import type { AppData } from '@/models/share'
+import AnswerIcon from '@/app/components/base/answer-icon'
 
 type AnswerProps = {
   item: ChatItem
@@ -89,11 +90,7 @@ const Answer: FC<AnswerProps> = ({
     <div className='flex mb-2 last:mb-0'>
       <div className='shrink-0 relative w-10 h-10'>
         {
-          answerIcon || (
-            <div className='flex items-center justify-center w-full h-full rounded-full bg-[#d5f5f6] border-[0.5px] border-black/5 text-xl'>
-              🤖
-            </div>
-          )
+          answerIcon || <AnswerIcon />
         }
         {
           responding && (

+ 13 - 1
web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx

@@ -15,6 +15,7 @@ import {
   stopChatMessageResponding,
 } from '@/service/share'
 import LogoAvatar from '@/app/components/base/logo/logo-embeded-chat-avatar'
+import AnswerIcon from '@/app/components/base/answer-icon'
 
 const ChatWrapper = () => {
   const {
@@ -114,6 +115,17 @@ const ChatWrapper = () => {
     return null
   }, [currentConversationId, inputsForms, isMobile])
 
+  const answerIcon = isDify()
+    ? <LogoAvatar className='relative shrink-0' />
+    : (appData?.site && appData.site.use_icon_as_answer_icon)
+      ? <AnswerIcon
+        iconType={appData.site.icon_type}
+        icon={appData.site.icon}
+        background={appData.site.icon_background}
+        imageUrl={appData.site.icon_url}
+      />
+      : null
+
   return (
     <Chat
       appData={appData}
@@ -129,7 +141,7 @@ const ChatWrapper = () => {
       allToolIcons={appMeta?.tool_icons || {}}
       onFeedback={handleFeedback}
       suggestedQuestions={suggestedQuestions}
-      answerIcon={isDify() ? <LogoAvatar className='relative shrink-0' /> : null}
+      answerIcon={answerIcon}
       hideProcessDetail
       themeBuilder={themeBuilder}
     />

+ 21 - 0
web/app/components/explore/create-app-modal/index.tsx

@@ -5,6 +5,7 @@ import { RiCloseLine } from '@remixicon/react'
 import AppIconPicker from '../../base/app-icon-picker'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
+import Switch from '@/app/components/base/switch'
 import Toast from '@/app/components/base/toast'
 import AppIcon from '@/app/components/base/app-icon'
 import { useProviderContext } from '@/context/provider-context'
@@ -20,12 +21,15 @@ export type CreateAppModalProps = {
   appIcon: string
   appIconBackground?: string | null
   appIconUrl?: string | null
+  appMode?: string
+  appUseIconAsAnswerIcon?: boolean
   onConfirm: (info: {
     name: string
     icon_type: AppIconType
     icon: string
     icon_background?: string
     description: string
+    use_icon_as_answer_icon?: boolean
   }) => Promise<void>
   onHide: () => void
 }
@@ -39,6 +43,8 @@ const CreateAppModal = ({
   appIconUrl,
   appName,
   appDescription,
+  appMode,
+  appUseIconAsAnswerIcon,
   onConfirm,
   onHide,
 }: CreateAppModalProps) => {
@@ -52,6 +58,7 @@ const CreateAppModal = ({
   )
   const [showAppIconPicker, setShowAppIconPicker] = useState(false)
   const [description, setDescription] = useState(appDescription || '')
+  const [useIconAsAnswerIcon, setUseIconAsAnswerIcon] = useState(appUseIconAsAnswerIcon || false)
 
   const { plan, enableBilling } = useProviderContext()
   const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
@@ -67,6 +74,7 @@ const CreateAppModal = ({
       icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
       icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined,
       description,
+      use_icon_as_answer_icon: useIconAsAnswerIcon,
     })
     onHide()
   }
@@ -119,6 +127,19 @@ const CreateAppModal = ({
               onChange={e => setDescription(e.target.value)}
             />
           </div>
+          {/* answer icon */}
+          {isEditModal && (appMode === 'chat' || appMode === 'advanced-chat' || appMode === 'agent-chat') && (
+            <div className='pt-2'>
+              <div className='flex justify-between items-center'>
+                <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.answerIcon.title')}</div>
+                <Switch
+                  defaultValue={useIconAsAnswerIcon}
+                  onChange={v => setUseIconAsAnswerIcon(v)}
+                />
+              </div>
+              <p className='body-xs-regular text-gray-500'>{t('app.answerIcon.descriptionInExplore')}</p>
+            </div>
+          )}
           {!isEditModal && isAppsFull && <AppsFull loc='app-explore-create' />}
         </div>
         <div className='flex flex-row-reverse'>

+ 5 - 0
web/i18n/en-US/app.ts

@@ -77,6 +77,11 @@ const translation = {
     emoji: 'Emoji',
     image: 'Image',
   },
+  answerIcon: {
+    title: 'Use WebApp icon to replace 🤖',
+    description: 'Wether to use the WebApp icon to replace 🤖 in the shared application',
+    descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore',
+  },
   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 ',
   switchTip: 'not allow',

+ 5 - 0
web/i18n/zh-Hans/app.ts

@@ -76,6 +76,11 @@ const translation = {
     emoji: '表情符号',
     image: '图片',
   },
+  answerIcon: {
+    title: '使用 WebApp 图标替换 🤖',
+    description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖',
+    descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖',
+  },
   switch: '迁移为工作流编排',
   switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将',
   switchTip: '不能够',

+ 1 - 0
web/models/explore.ts

@@ -8,6 +8,7 @@ export type AppBasicInfo = {
   icon_url: string
   name: string
   description: string
+  use_icon_as_answer_icon: boolean
 }
 
 export type AppCategory = 'Writing' | 'Translate' | 'HR' | 'Programming' | 'Assistant'

+ 1 - 0
web/models/share.ts

@@ -25,6 +25,7 @@ export type SiteInfo = {
   privacy_policy?: string
   custom_disclaimer?: string
   show_workflow_steps?: boolean
+  use_icon_as_answer_icon?: boolean
 }
 
 export type AppMeta = {

+ 2 - 2
web/service/apps.ts

@@ -28,8 +28,8 @@ export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: A
   return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } })
 }
 
-export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string }> = ({ appID, name, icon_type, icon, icon_background, description }) => {
-  return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description } })
+export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string; description: string; use_icon_as_answer_icon?: boolean }> = ({ appID, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon }) => {
+  return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon_type, icon, icon_background, description, use_icon_as_answer_icon } })
 }
 
 export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppMode; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {

+ 3 - 0
web/types/app.ts

@@ -297,6 +297,7 @@ export type SiteConfig = {
   icon_url: string | null
 
   show_workflow_steps: boolean
+  use_icon_as_answer_icon: boolean
 }
 
 export type AppIconType = 'image' | 'emoji'
@@ -323,6 +324,8 @@ export type App = {
   icon_background: string | null
   /** Icon URL, only available when icon_type is 'image' */
   icon_url: string | null
+  /** Whether to use app icon as answer icon */
+  use_icon_as_answer_icon: boolean
 
   /** Mode */
   mode: AppMode