浏览代码

Feature/add emoji (#103)

crazywoola 1 年之前
父节点
当前提交
37c3b8979c

+ 1 - 1
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx

@@ -49,7 +49,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
     return null
   return (
     <div className={cn(s.app, 'flex', 'overflow-hidden')}>
-      <AppSideBar title={response.name} desc={appModeName} navigation={navigation} />
+      <AppSideBar title={response.name} icon={response.icon} icon_background={response.icon_background} desc={appModeName} navigation={navigation} />
       <div className="bg-white grow">{children}</div>
     </div>
   )

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

@@ -47,7 +47,7 @@ const AppCard = ({
     <>
       <Link href={`/app/${app.id}/overview`} className={style.listItem}>
         <div className={style.listItemTitle}>
-          <AppIcon size='small' />
+          <AppIcon size='small' icon={app.icon} background={app.icon_background}/>
           <div className={style.listItemHeading}>
             <div className={style.listItemHeadingContent}>{app.name}</div>
           </div>

+ 1 - 0
web/app/(commonLayout)/apps/Apps.tsx

@@ -17,6 +17,7 @@ const Apps = () => {
       {apps.map(app => (<AppCard key={app.id} app={app} />))}
       <NewAppCard />
     </nav>
+
   )
 }
 

+ 0 - 1
web/app/(commonLayout)/apps/NewAppCard.tsx

@@ -9,7 +9,6 @@ import NewAppDialog from './NewAppDialog'
 const CreateAppCard = () => {
   const { t } = useTranslation()
   const [showNewAppDialog, setShowNewAppDialog] = useState(false)
-
   return (
     <a className={classNames(style.listItem, style.newItemCard)} onClick={() => setShowNewAppDialog(true)}>
       <div className={style.listItemTitle}>

+ 24 - 4
web/app/(commonLayout)/apps/NewAppDialog.tsx

@@ -17,6 +17,8 @@ import { createApp, fetchAppTemplates } from '@/service/apps'
 import AppIcon from '@/app/components/base/app-icon'
 import AppsContext from '@/context/app-context'
 
+import EmojiPicker from '@/app/components/base/emoji-picker'
+
 type NewAppDialogProps = {
   show: boolean
   onClose?: () => void
@@ -31,6 +33,11 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
   const [newAppMode, setNewAppMode] = useState<AppMode>()
   const [isWithTemplate, setIsWithTemplate] = useState(false)
   const [selectedTemplateIndex, setSelectedTemplateIndex] = useState<number>(-1)
+
+  // Emoji Picker
+  const [showEmojiPicker, setShowEmojiPicker] = useState(false)
+  const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' })
+
   const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
 
   const { data: templates, mutate } = useSWR({ url: '/app-templates' }, fetchAppTemplates)
@@ -67,6 +74,8 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
     try {
       const app = await createApp({
         name,
+        icon: emoji.icon,
+        icon_background: emoji.icon_background,
         mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!,
         config: isWithTemplate ? templates.data[selectedTemplateIndex].model_config : undefined,
       })
@@ -80,9 +89,20 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
       notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
     }
     isCreatingRef.current = false
-  }, [isWithTemplate, newAppMode, notify, router, templates, selectedTemplateIndex])
+  }, [isWithTemplate, newAppMode, notify, router, templates, selectedTemplateIndex, emoji])
 
-  return (
+  return <>
+    {showEmojiPicker && <EmojiPicker
+      onSelect={(icon, icon_background) => {
+        console.log(icon, icon_background)
+        setEmoji({ icon, icon_background })
+        setShowEmojiPicker(false)
+      }}
+      onClose={() => {
+        setEmoji({ icon: '🍌', icon_background: '#FFEAD5' })
+        setShowEmojiPicker(false)
+      }}
+    />}
     <Dialog
       show={show}
       title={t('app.newApp.startToCreate')}
@@ -96,7 +116,7 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
       <h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
 
       <div className='flex items-center justify-between gap-3 mb-8'>
-        <AppIcon size='large' />
+        <AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
         <input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' />
       </div>
 
@@ -187,7 +207,7 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
           )}
       </div>
     </Dialog>
-  )
+  </>
 }
 
 export default NewAppDialog

+ 2 - 0
web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout.tsx

@@ -155,6 +155,8 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
     <div className='flex' style={{ height: 'calc(100vh - 56px)' }}>
       {!hideSideBar && <AppSideBar
         title={datasetRes?.name || '--'}
+        icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
+        icon_background={datasetRes?.icon_background || '#F5F5F5'}
         desc={datasetRes?.description || '--'}
         navigation={navigation}
         extraInfo={<ExtraInfo />}

+ 11 - 5
web/app/components/app-sidebar/basic.tsx

@@ -15,7 +15,8 @@ export function randomString(length: number) {
 
 export type IAppBasicProps = {
   iconType?: 'app' | 'api' | 'dataset'
-  iconUrl?: string
+  icon?: string,
+  icon_background?: string,
   name: string
   type: string | React.ReactNode
   hoverTip?: string
@@ -41,15 +42,20 @@ const ICON_MAP = {
   'dataset': <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />
 }
 
-export default function AppBasic({ iconUrl, name, type, hoverTip, textStyle, iconType = 'app' }: IAppBasicProps) {
+export default function AppBasic({ icon, icon_background, name, type, hoverTip, textStyle, iconType = 'app' }: IAppBasicProps) {
   return (
     <div className="flex items-start">
-      {iconUrl && (
+      {icon && icon_background && iconType === 'app' && (
         <div className='flex-shrink-0 mr-3'>
-          {/* <img className="inline-block rounded-lg h-9 w-9" src={iconUrl} alt={name} /> */}
-          {ICON_MAP[iconType]}
+          <AppIcon icon={icon} background={icon_background} />
         </div>
       )}
+      {iconType !== 'app' &&
+        <div className='flex-shrink-0 mr-3'>
+          {ICON_MAP[iconType]}
+        </div>
+
+      }
       <div className="group">
         <div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 ${textStyle?.main}`}>
           {name}

+ 4 - 3
web/app/components/app-sidebar/index.tsx

@@ -7,6 +7,8 @@ export type IAppDetailNavProps = {
   iconType?: 'app' | 'dataset'
   title: string
   desc: string
+  icon: string
+  icon_background: string
   navigation: Array<{
     name: string
     href: string
@@ -16,13 +18,12 @@ export type IAppDetailNavProps = {
   extraInfo?: React.ReactNode
 }
 
-const sampleAppIconUrl = 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80'
 
-const AppDetailNav: FC<IAppDetailNavProps> = ({ title, desc, navigation, extraInfo, iconType = 'app' }) => {
+const AppDetailNav: FC<IAppDetailNavProps> = ({ title, desc, icon, icon_background, navigation, extraInfo, iconType = 'app' }) => {
   return (
     <div className="flex flex-col w-56 overflow-y-auto bg-white border-r border-gray-200 shrink-0">
       <div className="flex flex-shrink-0 p-4">
-        <AppBasic iconType={iconType} iconUrl={sampleAppIconUrl} name={title} type={desc} />
+        <AppBasic iconType={iconType} icon={icon} icon_background={icon_background} name={title} type={desc} />
       </div>
       <nav className="flex-1 p-4 space-y-1 bg-white">
         {navigation.map((item, index) => {

+ 2 - 4
web/app/components/app/overview/appCard.tsx

@@ -29,9 +29,6 @@ export type IAppCardProps = {
   onGenerateCode?: () => Promise<any>
 }
 
-// todo: get image url from appInfo
-const defaultUrl = 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80'
-
 function AppCard({
   appInfo,
   cardType = 'app',
@@ -104,7 +101,8 @@ function AppCard({
         <div className="mb-2.5 flex flex-row items-start justify-between">
           <AppBasic
             iconType={isApp ? 'app' : 'api'}
-            iconUrl={defaultUrl}
+            icon={appInfo.icon}
+            icon_background={appInfo.icon_background}
             name={basicName}
             type={
               isApp

+ 10 - 1
web/app/components/base/app-icon/index.tsx

@@ -2,6 +2,11 @@ import type { FC } from 'react'
 import classNames from 'classnames'
 import style from './style.module.css'
 
+import data from '@emoji-mart/data'
+import { init } from 'emoji-mart'
+
+init({ data })
+
 export type AppIconProps = {
   size?: 'tiny' | 'small' | 'medium' | 'large'
   rounded?: boolean
@@ -9,14 +14,17 @@ export type AppIconProps = {
   background?: string
   className?: string
   innerIcon?: React.ReactNode
+  onClick?: () => void
 }
 
 const AppIcon: FC<AppIconProps> = ({
   size = 'medium',
   rounded = false,
+  icon,
   background,
   className,
   innerIcon,
+  onClick,
 }) => {
   return (
     <span
@@ -29,8 +37,9 @@ const AppIcon: FC<AppIconProps> = ({
       style={{
         background,
       }}
+      onClick={onClick}
     >
-      {innerIcon ? innerIcon : <>🤖</>}
+      {innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id={'banana'} />}
     </span>
   )
 }

+ 204 - 0
web/app/components/base/emoji-picker/index.tsx

@@ -0,0 +1,204 @@
+'use client'
+import data from '@emoji-mart/data'
+import { init, SearchIndex } from 'emoji-mart'
+// import AppIcon from '@/app/components/base/app-icon'
+import cn from 'classnames'
+import Divider from '@/app/components/base/divider'
+
+import Button from '@/app/components/base/button'
+import s from './style.module.css'
+import { useState, FC, ChangeEvent } from 'react'
+import {
+  MagnifyingGlassIcon
+} from '@heroicons/react/24/outline'
+import React from 'react'
+import Modal from '@/app/components/base/modal'
+
+declare global {
+  namespace JSX {
+    interface IntrinsicElements {
+      'em-emoji': React.DetailedHTMLProps<
+        React.HTMLAttributes<HTMLElement>,
+        HTMLElement
+      >;
+    }
+  }
+}
+
+init({ data })
+
+async function search(value: string) {
+  const emojis = await SearchIndex.search(value) || []
+
+  const results = emojis.map((emoji: any) => {
+    return emoji.skins[0].native
+  })
+  return results
+}
+
+const backgroundColors = [
+  '#FFEAD5',
+  '#E4FBCC',
+  '#D3F8DF',
+  '#E0F2FE',
+
+  '#E0EAFF',
+  '#EFF1F5',
+  '#FBE8FF',
+  '#FCE7F6',
+
+  '#FEF7C3',
+  '#E6F4D7',
+  '#D5F5F6',
+  '#D1E9FF',
+
+  '#D1E0FF',
+  '#D5D9EB',
+  '#ECE9FE',
+  '#FFE4E8',
+]
+interface IEmojiPickerProps {
+  isModal?: boolean
+  onSelect?: (emoji: string, background: string) => void
+  onClose?: () => void
+}
+
+const EmojiPicker: FC<IEmojiPickerProps> = ({
+  isModal = true,
+  onSelect,
+  onClose
+
+}) => {
+  const { categories } = data as any
+  const [selectedEmoji, setSelectedEmoji] = useState('')
+  const [selectedBackground, setSelectedBackground] = useState(backgroundColors[0])
+
+  const [searchedEmojis, setSearchedEmojis] = useState([])
+  const [isSearching, setIsSearching] = useState(false)
+
+  return isModal ? <Modal
+    onClose={() => { }}
+    isShow
+    closable={false}
+    className={cn(s.container, '!w-[362px] !p-0')}
+  >
+    <div className='flex flex-col items-center w-full p-3'>
+      <div className="relative w-full">
+        <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
+          <MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
+        </div>
+        <input
+          type="search"
+          id="search"
+          className='block w-full h-10 px-3 pl-10 text-sm font-normal bg-gray-100 rounded-lg'
+          placeholder="Search emojis..."
+          onChange={async (e: ChangeEvent<HTMLInputElement>) => {
+            if (e.target.value === '') {
+              setIsSearching(false)
+              return
+            } else {
+              setIsSearching(true)
+              const emojis = await search(e.target.value)
+              setSearchedEmojis(emojis)
+            }
+          }}
+        />
+      </div>
+    </div>
+    <Divider className='m-0 mb-3' />
+
+    <div className="w-full max-h-[200px] overflow-x-hidden overflow-y-auto px-3">
+      {isSearching && <>
+        <div key={`category-search`} className='flex flex-col'>
+          <p className='font-medium uppercase text-xs text-[#101828] mb-1'>Search</p>
+          <div className='w-full h-full grid grid-cols-8 gap-1'>
+            {searchedEmojis.map((emoji: string, index: number) => {
+              return <div
+                key={`emoji-search-${index}`}
+                className='inline-flex w-10 h-10 rounded-lg items-center justify-center'
+                onClick={() => {
+                  setSelectedEmoji(emoji)
+                }}
+              >
+                <div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'>
+                  <em-emoji id={emoji} />
+                </div>
+              </div>
+            })}
+          </div>
+        </div>
+      </>}
+
+
+      {categories.map((category: any, index: number) => {
+        return <div key={`category-${index}`} className='flex flex-col'>
+          <p className='font-medium uppercase text-xs text-[#101828] mb-1'>{category.id}</p>
+          <div className='w-full h-full grid grid-cols-8 gap-1'>
+            {category.emojis.map((emoji: string, index: number) => {
+              return <div
+                key={`emoji-${index}`}
+                className='inline-flex w-10 h-10 rounded-lg items-center justify-center'
+                onClick={() => {
+                  setSelectedEmoji(emoji)
+                }}
+              >
+                <div className='cursor-pointer w-8 h-8 p-1 flex items-center justify-center rounded-lg hover:ring-1 ring-offset-1 ring-gray-300'>
+                  <em-emoji id={emoji} />
+                </div>
+              </div>
+            })}
+
+          </div>
+        </div>
+      })}
+    </div>
+
+    {/* Color Select */}
+    <div className={cn('flex flex-col p-3 ', selectedEmoji == '' ? 'opacity-25' : '')}>
+      <p className='font-medium uppercase text-xs text-[#101828] mb-2'>Choose Style</p>
+      <div className='w-full h-full grid grid-cols-8 gap-1'>
+        {backgroundColors.map((color) => {
+          return <div
+            key={color}
+            className={
+              cn(
+                'cursor-pointer',
+                `hover:ring-1 ring-offset-1`,
+                'inline-flex w-10 h-10 rounded-lg items-center justify-center',
+                color === selectedBackground ? `ring-1 ring-gray-300` : '',
+              )}
+            onClick={() => {
+              setSelectedBackground(color)
+            }}
+          >
+            <div className={cn(
+              'w-8 h-8 p-1 flex items-center justify-center rounded-lg',
+            )
+            } style={{ background: color }}>
+              {selectedEmoji !== '' && <em-emoji id={selectedEmoji} />}
+            </div>
+          </div>
+        })}
+      </div>
+    </div>
+    <Divider className='m-0' />
+    <div className='w-full flex items-center justify-center p-3 gap-2'>
+      <Button type="default" className='w-full' onClick={() => {
+        onClose && onClose()
+      }}>
+        Cancel
+      </Button>
+      <Button
+        disabled={selectedEmoji == ''}
+        type="primary"
+        className='w-full'
+        onClick={() => {
+          onSelect && onSelect(selectedEmoji, selectedBackground)
+        }}>
+        OK
+      </Button>
+    </div>
+  </Modal> : <>
+  </>
+}
+export default EmojiPicker

+ 12 - 0
web/app/components/base/emoji-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;
+}

+ 45 - 45
web/app/components/base/modal/index.tsx

@@ -25,51 +25,51 @@ export default function Modal({
   closable = false,
 }: IModal) {
   return (
-        <Transition appear show={isShow} as={Fragment}>
-            <Dialog as="div" className="relative z-10" onClose={onClose}>
-                <Transition.Child
-                    as={Fragment}
-                    enter="ease-out duration-300"
-                    enterFrom="opacity-0"
-                    enterTo="opacity-100"
-                    leave="ease-in duration-200"
-                    leaveFrom="opacity-100"
-                    leaveTo="opacity-0"
-                >
-                    <div className="fixed inset-0 bg-black bg-opacity-25" />
-                </Transition.Child>
+    <Transition appear show={isShow} as={Fragment}>
+      <Dialog as="div" className="relative z-10" onClose={onClose}>
+        <Transition.Child
+          as={Fragment}
+          enter="ease-out duration-300"
+          enterFrom="opacity-0"
+          enterTo="opacity-100"
+          leave="ease-in duration-200"
+          leaveFrom="opacity-100"
+          leaveTo="opacity-0"
+        >
+          <div className="fixed inset-0 bg-black bg-opacity-25" />
+        </Transition.Child>
 
-                <div className="fixed inset-0 overflow-y-auto">
-                    <div className={`flex min-h-full items-center justify-center p-4 text-center ${wrapperClassName}`}>
-                        <Transition.Child
-                            as={Fragment}
-                            enter="ease-out duration-300"
-                            enterFrom="opacity-0 scale-95"
-                            enterTo="opacity-100 scale-100"
-                            leave="ease-in duration-200"
-                            leaveFrom="opacity-100 scale-100"
-                            leaveTo="opacity-0 scale-95"
-                        >
-                            <Dialog.Panel className={`w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all ${className}`}>
-                                {title && <Dialog.Title
-                                    as="h3"
-                                    className="text-lg font-medium leading-6 text-gray-900"
-                                >
-                                    {title}
-                                </Dialog.Title>}
-                                {description && <Dialog.Description className='text-gray-500 text-xs font-normal mt-2'>
-                                    {description}
-                                </Dialog.Description>}
-                                {closable
-                                    && <div className='absolute top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
-                                        <XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
-                                    </div>}
-                                {children}
-                            </Dialog.Panel>
-                        </Transition.Child>
-                    </div>
-                </div>
-            </Dialog>
-        </Transition>
+        <div className="fixed inset-0 overflow-y-auto">
+          <div className="flex min-h-full items-center justify-center p-4 text-center">
+            <Transition.Child
+              as={Fragment}
+              enter="ease-out duration-300"
+              enterFrom="opacity-0 scale-95"
+              enterTo="opacity-100 scale-100"
+              leave="ease-in duration-200"
+              leaveFrom="opacity-100 scale-100"
+              leaveTo="opacity-0 scale-95"
+            >
+              <Dialog.Panel className={`w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all ${className}`}>
+                {title && <Dialog.Title
+                  as="h3"
+                  className="text-lg font-medium leading-6 text-gray-900"
+                >
+                  {title}
+                </Dialog.Title>}
+                {description && <Dialog.Description className='text-gray-500 text-xs font-normal mt-2'>
+                  {description}
+                </Dialog.Description>}
+                {closable
+                  && <div className='absolute top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
+                    <XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
+                  </div>}
+                {children}
+              </Dialog.Panel>
+            </Transition.Child>
+          </div>
+        </div>
+      </Dialog>
+    </Transition>
   )
 }

+ 8 - 4
web/app/components/header/index.tsx

@@ -69,11 +69,13 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
             text={t('common.menus.apps')}
             activeSegment={['apps', 'app']}
             link='/apps'
-            curNav={curApp && { id: curApp.id, name: curApp.name }}
+            curNav={curApp && { id: curApp.id, name: curApp.name ,icon: curApp.icon, icon_background: curApp.icon_background}}
             navs={appItems.map(item => ({
               id: item.id,
               name: item.name,
-              link: `/app/${item.id}/overview`
+              link: `/app/${item.id}/overview`,
+              icon: item.icon,
+              icon_background: item.icon_background
             }))}
             createText={t('common.menus.newApp')}
             onCreate={() => setShowNewAppDialog(true)}
@@ -91,11 +93,13 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
             text={t('common.menus.datasets')}
             activeSegment='datasets'
             link='/datasets'
-            curNav={currentDataset && { id: currentDataset.id, name: currentDataset.name }}
+            curNav={currentDataset && { id: currentDataset.id, name: currentDataset.name, icon: currentDataset.icon, icon_background: currentDataset.icon_background }}
             navs={datasets.map(dataset => ({
               id: dataset.id,
               name: dataset.name,
-              link: `/datasets/${dataset.id}/documents`
+              link: `/datasets/${dataset.id}/documents`,
+              icon: dataset.icon,
+              icon_background: dataset.icon_background
             }))}
             createText={t('common.menus.newDataset')}
             onCreate={() => router.push('/datasets/create')}

+ 4 - 2
web/app/components/header/nav/nav-selector/index.tsx

@@ -10,6 +10,8 @@ type NavItem = {
   id: string
   name: string
   link: string
+  icon: string
+  icon_background: string
 }
 export interface INavSelectorProps {
   navs: NavItem[]
@@ -66,7 +68,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate }: INavSelectorProps)
                   <Menu.Item key={nav.id}>
                     <div className={itemClassName} onClick={() => router.push(nav.link)}>
                       <div className='relative w-6 h-6 mr-2 bg-[#D5F5F6] rounded-[6px]'>
-                        <AppIcon size='tiny' />
+                        <AppIcon size='tiny' icon={nav.icon} background={nav.icon_background}/>
                         <div className='flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded'>
                           <Indicator />
                         </div>
@@ -102,4 +104,4 @@ const NavSelector = ({ curNav, navs, createText, onCreate }: INavSelectorProps)
   )
 }
 
-export default NavSelector
+export default NavSelector

+ 2 - 0
web/app/components/share/chat/index.tsx

@@ -441,6 +441,8 @@ const Main: FC<IMainProps> = () => {
     <div className='bg-gray-100'>
       <Header
         title={siteInfo.title}
+        icon={siteInfo.icon || ''}
+        icon_background={siteInfo.icon_background || '#FFEAD5'}
         isMobile={isMobile}
         onShowSideBar={showSidebar}
         onCreateNewChat={() => handleConversationIdChange('-1')}

+ 5 - 1
web/app/components/share/header.tsx

@@ -7,6 +7,8 @@ import {
 } from '@heroicons/react/24/solid'
 export type IHeaderProps = {
   title: string
+  icon: string
+  icon_background: string
   isMobile?: boolean
   onShowSideBar?: () => void
   onCreateNewChat?: () => void
@@ -14,6 +16,8 @@ export type IHeaderProps = {
 const Header: FC<IHeaderProps> = ({
   title,
   isMobile,
+  icon,
+  icon_background,
   onShowSideBar,
   onCreateNewChat,
 }) => {
@@ -28,7 +32,7 @@ const Header: FC<IHeaderProps> = ({
         </div>
       ) : <div></div>}
       <div className='flex items-center space-x-2'>
-        <AppIcon size="small" />
+        <AppIcon size="small" icon={icon} background={icon_background} />
         <div className=" text-sm text-gray-800 font-bold">{title}</div>
       </div>
       {isMobile ? (

+ 2 - 0
web/models/datasets.ts

@@ -3,6 +3,8 @@ import { AppMode } from './app'
 export type DataSet = {
   id: string
   name: string
+  icon: string
+  icon_background: string
   description: string
   permission: 'only_me' | 'all_team_members'
   data_source_type: 'upload_file'

+ 2 - 0
web/models/share.ts

@@ -11,6 +11,8 @@ export type ConversationItem = {
 
 export type SiteInfo = {
   title: string
+  icon: string
+  icon_background: string
   description: string
   default_language: Locale
   prompt_public: boolean

+ 2 - 0
web/package.json

@@ -10,6 +10,7 @@
     "fix": "next lint --fix"
   },
   "dependencies": {
+    "@emoji-mart/data": "^1.1.2",
     "@formatjs/intl-localematcher": "^0.2.32",
     "@headlessui/react": "^1.7.13",
     "@heroicons/react": "^2.0.16",
@@ -33,6 +34,7 @@
     "dayjs": "^1.11.7",
     "echarts": "^5.4.1",
     "echarts-for-react": "^3.0.2",
+    "emoji-mart": "^5.5.2",
     "eslint": "8.36.0",
     "eslint-config-next": "13.2.4",
     "i18next": "^22.4.13",

+ 2 - 2
web/service/apps.ts

@@ -16,8 +16,8 @@ export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> =
   return get(url) as Promise<AppTemplatesResponse>
 }
 
-export const createApp: Fetcher<AppDetailResponse, { name: string; mode: AppMode; config?: ModelConfig }> = ({ name, mode, config }) => {
-  return post('apps', { body: { name, mode, model_config: config } }) as Promise<AppDetailResponse>
+export const createApp: Fetcher<AppDetailResponse, { name: string; icon: string, icon_background: string, mode: AppMode; config?: ModelConfig }> = ({ name, icon, icon_background, mode, config }) => {
+  return post('apps', { body: { name, icon, icon_background, mode, model_config: config } }) as Promise<AppDetailResponse>
 }
 
 export const deleteApp: Fetcher<CommonResponse, string> = (appID) => {

+ 6 - 0
web/types/app.ts

@@ -190,6 +190,12 @@ export type App = {
   id: string
   /** Name */
   name: string
+
+  /** Icon */
+  icon: string
+  /** Icon Background */
+  icon_background: string
+  
   /** Mode */
   mode: AppMode
   /** Enable web app */