| 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> | ||||
| ) | ) |
| <> | <> | ||||
| <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> |
| {apps.map(app => (<AppCard key={app.id} app={app} />))} | {apps.map(app => (<AppCard key={app.id} app={app} />))} | ||||
| <NewAppCard /> | <NewAppCard /> | ||||
| </nav> | </nav> | ||||
| ) | ) | ||||
| } | } | ||||
| 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}> |
| 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 | ||||
| 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) | ||||
| 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, | ||||
| }) | }) | ||||
| 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')} | ||||
| <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> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| </Dialog> | </Dialog> | ||||
| ) | |||||
| </> | |||||
| } | } | ||||
| export default NewAppDialog | export default NewAppDialog |
| <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 />} |
| 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 | ||||
| '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} /> */} | |||||
| {ICON_MAP[iconType]} | |||||
| <AppIcon icon={icon} background={icon_background} /> | |||||
| </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} |
| 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 | ||||
| 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) => { |
| 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', | ||||
| <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 |
| 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 | ||||
| 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 | ||||
| style={{ | style={{ | ||||
| background, | background, | ||||
| }} | }} | ||||
| onClick={onClick} | |||||
| > | > | ||||
| {innerIcon ? innerIcon : <>🤖</>} | |||||
| {innerIcon ? innerIcon : icon && icon !== '' ? <em-emoji id={icon} /> : <em-emoji id={'banana'} />} | |||||
| </span> | </span> | ||||
| ) | ) | ||||
| } | } |
| '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 |
| .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; | |||||
| } |
| closable = false, | closable = false, | ||||
| }: IModal) { | }: IModal) { | ||||
| return ( | 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> | |||||
| ) | ) | ||||
| } | } |
| 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)} | ||||
| 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')} |
| 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[] | ||||
| <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> | ||||
| ) | ) | ||||
| } | } | ||||
| export default NavSelector | |||||
| export default NavSelector |
| <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')} |
| } 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 | ||||
| const Header: FC<IHeaderProps> = ({ | const Header: FC<IHeaderProps> = ({ | ||||
| title, | title, | ||||
| isMobile, | isMobile, | ||||
| icon, | |||||
| icon_background, | |||||
| onShowSideBar, | onShowSideBar, | ||||
| onCreateNewChat, | onCreateNewChat, | ||||
| }) => { | }) => { | ||||
| </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 ? ( |
| 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' |
| 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 |
| "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", | ||||
| "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", |
| 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 }) => { | |||||
| 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) => { | export const deleteApp: Fetcher<CommonResponse, string> = (appID) => { |
| 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 */ |