mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 03:16:51 +08:00
feat: allow users to use the app icon as the answer icon (#7888)
Co-authored-by: crazywoola <427733928@qq.com>
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
web/app/components/base/answer-icon/index.tsx
Normal file
47
web/app/components/base/answer-icon/index.tsx
Normal file
@@ -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
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -76,6 +76,11 @@ const translation = {
|
||||
emoji: '表情符号',
|
||||
image: '图片',
|
||||
},
|
||||
answerIcon: {
|
||||
title: '使用 WebApp 图标替换 🤖',
|
||||
description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖',
|
||||
descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖',
|
||||
},
|
||||
switch: '迁移为工作流编排',
|
||||
switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将',
|
||||
switchTip: '不能够',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user