mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-12 20:39:01 +08:00
Feature/add emoji (#103)
This commit is contained in:
parent
f68b05d5ec
commit
37c3b8979c
@ -49,7 +49,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
return null
|
return null
|
||||||
return (
|
return (
|
||||||
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
|
<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 className="bg-white grow">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -47,7 +47,7 @@ const AppCard = ({
|
|||||||
<>
|
<>
|
||||||
<Link href={`/app/${app.id}/overview`} className={style.listItem}>
|
<Link href={`/app/${app.id}/overview`} className={style.listItem}>
|
||||||
<div className={style.listItemTitle}>
|
<div className={style.listItemTitle}>
|
||||||
<AppIcon size='small' />
|
<AppIcon size='small' icon={app.icon} background={app.icon_background}/>
|
||||||
<div className={style.listItemHeading}>
|
<div className={style.listItemHeading}>
|
||||||
<div className={style.listItemHeadingContent}>{app.name}</div>
|
<div className={style.listItemHeadingContent}>{app.name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,7 @@ const Apps = () => {
|
|||||||
{apps.map(app => (<AppCard key={app.id} app={app} />))}
|
{apps.map(app => (<AppCard key={app.id} app={app} />))}
|
||||||
<NewAppCard />
|
<NewAppCard />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import NewAppDialog from './NewAppDialog'
|
|||||||
const CreateAppCard = () => {
|
const CreateAppCard = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a className={classNames(style.listItem, style.newItemCard)} onClick={() => setShowNewAppDialog(true)}>
|
<a className={classNames(style.listItem, style.newItemCard)} onClick={() => setShowNewAppDialog(true)}>
|
||||||
<div className={style.listItemTitle}>
|
<div className={style.listItemTitle}>
|
||||||
|
@ -17,6 +17,8 @@ import { createApp, fetchAppTemplates } from '@/service/apps'
|
|||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import AppsContext from '@/context/app-context'
|
import AppsContext from '@/context/app-context'
|
||||||
|
|
||||||
|
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||||
|
|
||||||
type NewAppDialogProps = {
|
type NewAppDialogProps = {
|
||||||
show: boolean
|
show: boolean
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
@ -31,6 +33,11 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
|
|||||||
const [newAppMode, setNewAppMode] = useState<AppMode>()
|
const [newAppMode, setNewAppMode] = useState<AppMode>()
|
||||||
const [isWithTemplate, setIsWithTemplate] = useState(false)
|
const [isWithTemplate, setIsWithTemplate] = useState(false)
|
||||||
const [selectedTemplateIndex, setSelectedTemplateIndex] = useState<number>(-1)
|
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 mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||||
|
|
||||||
const { data: templates, mutate } = useSWR({ url: '/app-templates' }, fetchAppTemplates)
|
const { data: templates, mutate } = useSWR({ url: '/app-templates' }, fetchAppTemplates)
|
||||||
@ -67,6 +74,8 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
|
|||||||
try {
|
try {
|
||||||
const app = await createApp({
|
const app = await createApp({
|
||||||
name,
|
name,
|
||||||
|
icon: emoji.icon,
|
||||||
|
icon_background: emoji.icon_background,
|
||||||
mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!,
|
mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!,
|
||||||
config: isWithTemplate ? templates.data[selectedTemplateIndex].model_config : undefined,
|
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') })
|
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||||
}
|
}
|
||||||
isCreatingRef.current = false
|
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
|
<Dialog
|
||||||
show={show}
|
show={show}
|
||||||
title={t('app.newApp.startToCreate')}
|
title={t('app.newApp.startToCreate')}
|
||||||
@ -96,7 +116,7 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
|
|||||||
<h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
|
<h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
|
||||||
|
|
||||||
<div className='flex items-center justify-between gap-3 mb-8'>
|
<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' />
|
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -187,7 +207,7 @@ const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NewAppDialog
|
export default NewAppDialog
|
||||||
|
@ -155,6 +155,8 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
<div className='flex' style={{ height: 'calc(100vh - 56px)' }}>
|
<div className='flex' style={{ height: 'calc(100vh - 56px)' }}>
|
||||||
{!hideSideBar && <AppSideBar
|
{!hideSideBar && <AppSideBar
|
||||||
title={datasetRes?.name || '--'}
|
title={datasetRes?.name || '--'}
|
||||||
|
icon={datasetRes?.icon || 'https://static.dify.ai/images/dataset-default-icon.png'}
|
||||||
|
icon_background={datasetRes?.icon_background || '#F5F5F5'}
|
||||||
desc={datasetRes?.description || '--'}
|
desc={datasetRes?.description || '--'}
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
extraInfo={<ExtraInfo />}
|
extraInfo={<ExtraInfo />}
|
||||||
|
@ -15,7 +15,8 @@ export function randomString(length: number) {
|
|||||||
|
|
||||||
export type IAppBasicProps = {
|
export type IAppBasicProps = {
|
||||||
iconType?: 'app' | 'api' | 'dataset'
|
iconType?: 'app' | 'api' | 'dataset'
|
||||||
iconUrl?: string
|
icon?: string,
|
||||||
|
icon_background?: string,
|
||||||
name: string
|
name: string
|
||||||
type: string | React.ReactNode
|
type: string | React.ReactNode
|
||||||
hoverTip?: string
|
hoverTip?: string
|
||||||
@ -41,15 +42,20 @@ const ICON_MAP = {
|
|||||||
'dataset': <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />
|
'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 (
|
return (
|
||||||
<div className="flex items-start">
|
<div className="flex items-start">
|
||||||
{iconUrl && (
|
{icon && icon_background && iconType === 'app' && (
|
||||||
<div className='flex-shrink-0 mr-3'>
|
<div className='flex-shrink-0 mr-3'>
|
||||||
{/* <img className="inline-block rounded-lg h-9 w-9" src={iconUrl} alt={name} /> */}
|
<AppIcon icon={icon} background={icon_background} />
|
||||||
{ICON_MAP[iconType]}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{iconType !== 'app' &&
|
||||||
|
<div className='flex-shrink-0 mr-3'>
|
||||||
|
{ICON_MAP[iconType]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 ${textStyle?.main}`}>
|
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 ${textStyle?.main}`}>
|
||||||
{name}
|
{name}
|
||||||
|
@ -7,6 +7,8 @@ export type IAppDetailNavProps = {
|
|||||||
iconType?: 'app' | 'dataset'
|
iconType?: 'app' | 'dataset'
|
||||||
title: string
|
title: string
|
||||||
desc: string
|
desc: string
|
||||||
|
icon: string
|
||||||
|
icon_background: string
|
||||||
navigation: Array<{
|
navigation: Array<{
|
||||||
name: string
|
name: string
|
||||||
href: string
|
href: string
|
||||||
@ -16,13 +18,12 @@ export type IAppDetailNavProps = {
|
|||||||
extraInfo?: React.ReactNode
|
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 (
|
return (
|
||||||
<div className="flex flex-col w-56 overflow-y-auto bg-white border-r border-gray-200 shrink-0">
|
<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">
|
<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>
|
</div>
|
||||||
<nav className="flex-1 p-4 space-y-1 bg-white">
|
<nav className="flex-1 p-4 space-y-1 bg-white">
|
||||||
{navigation.map((item, index) => {
|
{navigation.map((item, index) => {
|
||||||
|
@ -29,9 +29,6 @@ export type IAppCardProps = {
|
|||||||
onGenerateCode?: () => Promise<any>
|
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({
|
function AppCard({
|
||||||
appInfo,
|
appInfo,
|
||||||
cardType = 'app',
|
cardType = 'app',
|
||||||
@ -104,7 +101,8 @@ function AppCard({
|
|||||||
<div className="mb-2.5 flex flex-row items-start justify-between">
|
<div className="mb-2.5 flex flex-row items-start justify-between">
|
||||||
<AppBasic
|
<AppBasic
|
||||||
iconType={isApp ? 'app' : 'api'}
|
iconType={isApp ? 'app' : 'api'}
|
||||||
iconUrl={defaultUrl}
|
icon={appInfo.icon}
|
||||||
|
icon_background={appInfo.icon_background}
|
||||||
name={basicName}
|
name={basicName}
|
||||||
type={
|
type={
|
||||||
isApp
|
isApp
|
||||||
|
@ -2,6 +2,11 @@ import type { FC } from 'react'
|
|||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import style from './style.module.css'
|
import style from './style.module.css'
|
||||||
|
|
||||||
|
import data from '@emoji-mart/data'
|
||||||
|
import { init } from 'emoji-mart'
|
||||||
|
|
||||||
|
init({ data })
|
||||||
|
|
||||||
export type AppIconProps = {
|
export type AppIconProps = {
|
||||||
size?: 'tiny' | 'small' | 'medium' | 'large'
|
size?: 'tiny' | 'small' | 'medium' | 'large'
|
||||||
rounded?: boolean
|
rounded?: boolean
|
||||||
@ -9,14 +14,17 @@ export type AppIconProps = {
|
|||||||
background?: string
|
background?: string
|
||||||
className?: string
|
className?: string
|
||||||
innerIcon?: React.ReactNode
|
innerIcon?: React.ReactNode
|
||||||
|
onClick?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppIcon: FC<AppIconProps> = ({
|
const AppIcon: FC<AppIconProps> = ({
|
||||||
size = 'medium',
|
size = 'medium',
|
||||||
rounded = false,
|
rounded = false,
|
||||||
|
icon,
|
||||||
background,
|
background,
|
||||||
className,
|
className,
|
||||||
innerIcon,
|
innerIcon,
|
||||||
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@ -29,8 +37,9 @@ const AppIcon: FC<AppIconProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
background,
|
background,
|
||||||
}}
|
}}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{innerIcon ? innerIcon : <>🤖</>}
|
{innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id={'banana'} />}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
204
web/app/components/base/emoji-picker/index.tsx
Normal file
204
web/app/components/base/emoji-picker/index.tsx
Normal file
@ -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
web/app/components/base/emoji-picker/style.module.css
Normal file
12
web/app/components/base/emoji-picker/style.module.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 362px;
|
||||||
|
max-height: 552px;
|
||||||
|
|
||||||
|
border: 0.5px solid #EAECF0;
|
||||||
|
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
@ -25,51 +25,51 @@ export default function Modal({
|
|||||||
closable = false,
|
closable = false,
|
||||||
}: IModal) {
|
}: IModal) {
|
||||||
return (
|
return (
|
||||||
<Transition appear show={isShow} as={Fragment}>
|
<Transition appear show={isShow} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
<Dialog as="div" className="relative z-10" onClose={onClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto">
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className={`flex min-h-full items-center justify-center p-4 text-center ${wrapperClassName}`}>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 scale-95"
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo="opacity-100 scale-100"
|
enterTo="opacity-100 scale-100"
|
||||||
leave="ease-in duration-200"
|
leave="ease-in duration-200"
|
||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
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}`}>
|
<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
|
{title && <Dialog.Title
|
||||||
as="h3"
|
as="h3"
|
||||||
className="text-lg font-medium leading-6 text-gray-900"
|
className="text-lg font-medium leading-6 text-gray-900"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Dialog.Title>}
|
</Dialog.Title>}
|
||||||
{description && <Dialog.Description className='text-gray-500 text-xs font-normal mt-2'>
|
{description && <Dialog.Description className='text-gray-500 text-xs font-normal mt-2'>
|
||||||
{description}
|
{description}
|
||||||
</Dialog.Description>}
|
</Dialog.Description>}
|
||||||
{closable
|
{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'>
|
&& <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} />
|
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
|
||||||
</div>}
|
</div>}
|
||||||
{children}
|
{children}
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition>
|
</Transition>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -69,11 +69,13 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
|
|||||||
text={t('common.menus.apps')}
|
text={t('common.menus.apps')}
|
||||||
activeSegment={['apps', 'app']}
|
activeSegment={['apps', 'app']}
|
||||||
link='/apps'
|
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 => ({
|
navs={appItems.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
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')}
|
createText={t('common.menus.newApp')}
|
||||||
onCreate={() => setShowNewAppDialog(true)}
|
onCreate={() => setShowNewAppDialog(true)}
|
||||||
@ -91,11 +93,13 @@ const Header: FC<IHeaderProps> = ({ appItems, curApp, userProfile, onLogout, lan
|
|||||||
text={t('common.menus.datasets')}
|
text={t('common.menus.datasets')}
|
||||||
activeSegment='datasets'
|
activeSegment='datasets'
|
||||||
link='/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 => ({
|
navs={datasets.map(dataset => ({
|
||||||
id: dataset.id,
|
id: dataset.id,
|
||||||
name: dataset.name,
|
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')}
|
createText={t('common.menus.newDataset')}
|
||||||
onCreate={() => router.push('/datasets/create')}
|
onCreate={() => router.push('/datasets/create')}
|
||||||
|
@ -10,6 +10,8 @@ type NavItem = {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
link: string
|
link: string
|
||||||
|
icon: string
|
||||||
|
icon_background: string
|
||||||
}
|
}
|
||||||
export interface INavSelectorProps {
|
export interface INavSelectorProps {
|
||||||
navs: NavItem[]
|
navs: NavItem[]
|
||||||
@ -66,7 +68,7 @@ const NavSelector = ({ curNav, navs, createText, onCreate }: INavSelectorProps)
|
|||||||
<Menu.Item key={nav.id}>
|
<Menu.Item key={nav.id}>
|
||||||
<div className={itemClassName} onClick={() => router.push(nav.link)}>
|
<div className={itemClassName} onClick={() => router.push(nav.link)}>
|
||||||
<div className='relative w-6 h-6 mr-2 bg-[#D5F5F6] rounded-[6px]'>
|
<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'>
|
<div className='flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded'>
|
||||||
<Indicator />
|
<Indicator />
|
||||||
</div>
|
</div>
|
||||||
@ -102,4 +104,4 @@ const NavSelector = ({ curNav, navs, createText, onCreate }: INavSelectorProps)
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NavSelector
|
export default NavSelector
|
||||||
|
@ -441,6 +441,8 @@ const Main: FC<IMainProps> = () => {
|
|||||||
<div className='bg-gray-100'>
|
<div className='bg-gray-100'>
|
||||||
<Header
|
<Header
|
||||||
title={siteInfo.title}
|
title={siteInfo.title}
|
||||||
|
icon={siteInfo.icon || ''}
|
||||||
|
icon_background={siteInfo.icon_background || '#FFEAD5'}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
onShowSideBar={showSidebar}
|
onShowSideBar={showSidebar}
|
||||||
onCreateNewChat={() => handleConversationIdChange('-1')}
|
onCreateNewChat={() => handleConversationIdChange('-1')}
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
} from '@heroicons/react/24/solid'
|
} from '@heroicons/react/24/solid'
|
||||||
export type IHeaderProps = {
|
export type IHeaderProps = {
|
||||||
title: string
|
title: string
|
||||||
|
icon: string
|
||||||
|
icon_background: string
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
onShowSideBar?: () => void
|
onShowSideBar?: () => void
|
||||||
onCreateNewChat?: () => void
|
onCreateNewChat?: () => void
|
||||||
@ -14,6 +16,8 @@ export type IHeaderProps = {
|
|||||||
const Header: FC<IHeaderProps> = ({
|
const Header: FC<IHeaderProps> = ({
|
||||||
title,
|
title,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
icon,
|
||||||
|
icon_background,
|
||||||
onShowSideBar,
|
onShowSideBar,
|
||||||
onCreateNewChat,
|
onCreateNewChat,
|
||||||
}) => {
|
}) => {
|
||||||
@ -28,7 +32,7 @@ const Header: FC<IHeaderProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : <div></div>}
|
) : <div></div>}
|
||||||
<div className='flex items-center space-x-2'>
|
<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 className=" text-sm text-gray-800 font-bold">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
|
@ -3,6 +3,8 @@ import { AppMode } from './app'
|
|||||||
export type DataSet = {
|
export type DataSet = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
icon: string
|
||||||
|
icon_background: string
|
||||||
description: string
|
description: string
|
||||||
permission: 'only_me' | 'all_team_members'
|
permission: 'only_me' | 'all_team_members'
|
||||||
data_source_type: 'upload_file'
|
data_source_type: 'upload_file'
|
||||||
|
@ -11,6 +11,8 @@ export type ConversationItem = {
|
|||||||
|
|
||||||
export type SiteInfo = {
|
export type SiteInfo = {
|
||||||
title: string
|
title: string
|
||||||
|
icon: string
|
||||||
|
icon_background: string
|
||||||
description: string
|
description: string
|
||||||
default_language: Locale
|
default_language: Locale
|
||||||
prompt_public: boolean
|
prompt_public: boolean
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"fix": "next lint --fix"
|
"fix": "next lint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@formatjs/intl-localematcher": "^0.2.32",
|
"@formatjs/intl-localematcher": "^0.2.32",
|
||||||
"@headlessui/react": "^1.7.13",
|
"@headlessui/react": "^1.7.13",
|
||||||
"@heroicons/react": "^2.0.16",
|
"@heroicons/react": "^2.0.16",
|
||||||
@ -33,6 +34,7 @@
|
|||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"echarts": "^5.4.1",
|
"echarts": "^5.4.1",
|
||||||
"echarts-for-react": "^3.0.2",
|
"echarts-for-react": "^3.0.2",
|
||||||
|
"emoji-mart": "^5.5.2",
|
||||||
"eslint": "8.36.0",
|
"eslint": "8.36.0",
|
||||||
"eslint-config-next": "13.2.4",
|
"eslint-config-next": "13.2.4",
|
||||||
"i18next": "^22.4.13",
|
"i18next": "^22.4.13",
|
||||||
|
@ -16,8 +16,8 @@ export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> =
|
|||||||
return get(url) as Promise<AppTemplatesResponse>
|
return get(url) as Promise<AppTemplatesResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createApp: Fetcher<AppDetailResponse, { name: string; mode: AppMode; config?: ModelConfig }> = ({ name, mode, config }) => {
|
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, mode, model_config: config } }) as Promise<AppDetailResponse>
|
return post('apps', { body: { name, icon, icon_background, mode, model_config: config } }) as Promise<AppDetailResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteApp: Fetcher<CommonResponse, string> = (appID) => {
|
export const deleteApp: Fetcher<CommonResponse, string> = (appID) => {
|
||||||
|
@ -190,6 +190,12 @@ export type App = {
|
|||||||
id: string
|
id: string
|
||||||
/** Name */
|
/** Name */
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
|
/** Icon */
|
||||||
|
icon: string
|
||||||
|
/** Icon Background */
|
||||||
|
icon_background: string
|
||||||
|
|
||||||
/** Mode */
|
/** Mode */
|
||||||
mode: AppMode
|
mode: AppMode
|
||||||
/** Enable web app */
|
/** Enable web app */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user