feat: support assistant frontend (#2139)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
This commit is contained in:
Joel 2024-01-23 19:31:56 +08:00 • committed by GitHub
commit 7bbe12b2bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
194 changed files with 8726 additions and 1586 deletions

View file

@ -38,15 +38,24 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const navigation = useMemo(() => {
const navs = [
{ name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon },
...(isCurrentWorkspaceManager ? [{ name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon }] : []),
{ name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon },
{ name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
{ name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
]
return navs
}, [appId, isCurrentWorkspaceManager, t])
const appModeName = response?.mode?.toUpperCase() === 'COMPLETION' ? t('common.appModes.completionApp') : t('common.appModes.chatApp')
const appModeName = (() => {
if (response?.mode?.toUpperCase() === 'COMPLETION')
return t('app.newApp.completeApp')
const isAgent = !!response?.is_agent
if (isAgent)
return t('appDebug.assistantType.agentAssistant.name')
return t('appDebug.assistantType.chatAssistant.name')
})()
useEffect(() => {
if (response?.name)
document.title = `${(response.name || 'App')} - Dify`

View file

@ -55,9 +55,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
catch (e: any) {
notify({
type: 'error',
message: `${t('app.appDeleteFailed')}${
'message' in e ? `: ${e.message}` : ''
}`,
message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
})
}
setShowConfirmDelete(false)
@ -141,7 +139,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
if (showSettingsModal)
return
e.preventDefault()
push(`/app/${app.id}/overview`)
push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
}}
className={style.listItem}
>
@ -173,7 +172,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
{app.model_config?.pre_prompt}
</div>
<div className={style.listItemFooter}>
<AppModeLabel mode={app.mode} />
<AppModeLabel mode={app.mode} isAgent={app.is_agent} />
</div>
{showConfirmDelete && (

View file

@ -1,25 +1,53 @@
'use client'
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import style from '../list.module.css'
import { type AppMode } from '@/types/app'
import {
AiText,
CuteRobote,
} from '@/app/components/base/icons/src/vender/solid/communication'
import { BubbleText } from '@/app/components/base/icons/src/vender/solid/education'
export type AppModeLabelProps = {
mode: AppMode
isAgent?: boolean
className?: string
}
const AppModeLabel = ({
mode,
isAgent,
className,
}: AppModeLabelProps) => {
const { t } = useTranslation()
return (
<span className={classNames('flex items-center w-fit h-6 gap-1 px-2 text-gray-500 text-xs border border-gray-100 rounded', className)}>
<span className={classNames(style.listItemFooterIcon, mode === 'chat' && style.solidChatIcon, mode === 'completion' && style.solidCompletionIcon)} />
{t(`app.modes.${mode}`)}
</span>
<div className={`inline-flex items-center px-2 h-6 rounded-md border border-gray-100 text-xs text-gray-500 ${className}`}>
{
mode === 'completion' && (
<>
<AiText className='mr-1 w-3 h-3 text-gray-400' />
{t('app.newApp.completeApp')}
</>
)
}
{
mode === 'chat' && !isAgent && (
<>
<BubbleText className='mr-1 w-3 h-3 text-gray-400' />
{t('appDebug.assistantType.chatAssistant.name')}
</>
)
}
{
mode === 'chat' && isAgent && (
<>
<CuteRobote className='mr-1 w-3 h-3 text-gray-400' />
{t('appDebug.assistantType.agentAssistant.name')}
</>
)
}
</div>
)
}

View file

@ -1,8 +1,9 @@
'use client'
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import useSWRInfinite from 'swr/infinite'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import AppCard from './AppCard'
import NewAppCard from './NewAppCard'
import type { AppListResponse } from '@/models/app'
@ -10,17 +11,46 @@ import { fetchAppList } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { CheckModal } from '@/hooks/use-pay'
const getKey = (pageIndex: number, previousPageData: AppListResponse) => {
if (!pageIndex || previousPageData.has_more)
return { url: 'apps', params: { page: pageIndex + 1, limit: 30 } }
import TabSlider from '@/app/components/base/tab-slider'
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
const getKey = (
pageIndex: number,
previousPageData: AppListResponse,
activeTab: string,
keywords: string,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: any = { url: 'apps', params: { page: pageIndex + 1, limit: 30, name: keywords } }
if (activeTab !== 'all')
params.params.mode = activeTab
return params
}
return null
}
const Apps = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false })
const [activeTab, setActiveTab] = useState('all')
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const { data, isLoading, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, searchKeywords),
fetchAppList,
{ revalidateFirstPage: false },
)
const anchorRef = useRef<HTMLDivElement>(null)
const options = [
{ value: 'all', text: t('app.types.all') },
{ value: 'chat', text: t('app.types.assistant') },
{ value: 'completion', text: t('app.types.completion') },
]
useEffect(() => {
document.title = `${t('app.title')} - Dify`
@ -34,24 +64,72 @@ const Apps = () => {
let observer: IntersectionObserver | undefined
if (anchorRef.current) {
observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting)
setSize(size => size + 1)
if (entries[0].isIntersecting && !isLoading)
setSize((size: number) => size + 1)
}, { rootMargin: '100px' })
observer.observe(anchorRef.current)
}
return () => observer?.disconnect()
}, [isLoading, setSize, anchorRef, mutate])
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
}, { wait: 500 })
const handleKeywordsChange = (value: string) => {
setKeywords(value)
handleSearch()
}
const handleClear = () => {
handleKeywordsChange('')
}
return (
<><nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{ isCurrentWorkspaceManager
&& <NewAppCard onSuccess={mutate} />}
{data?.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={mutate} />
)))}
<CheckModal />
</nav>
<div ref={anchorRef} className='h-0'> </div>
<>
<div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
<div className="flex items-center px-2 w-[200px] h-8 rounded-lg bg-gray-200">
<div className="pointer-events-none shrink-0 flex items-center mr-1.5 justify-center w-4 h-4">
<SearchLg className="h-3.5 w-3.5 text-gray-500" aria-hidden="true" />
</div>
<input
type="text"
name="query"
className="grow block h-[18px] bg-gray-200 rounded-md border-0 text-gray-600 text-[13px] placeholder:text-gray-500 appearance-none outline-none"
placeholder={t('common.operation.search')!}
value={keywords}
onChange={(e) => {
handleKeywordsChange(e.target.value)
}}
autoComplete="off"
/>
{
keywords && (
<div
className='shrink-0 flex items-center justify-center w-4 h-4 cursor-pointer'
onClick={handleClear}
>
<XCircle className='w-3.5 h-3.5 text-gray-400' />
</div>
)
}
</div>
<TabSlider
value={activeTab}
onChange={setActiveTab}
options={options}
/>
</div>
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{isCurrentWorkspaceManager
&& <NewAppCard onSuccess={mutate} />}
{data?.map(({ data: apps }: any) => apps.map((app: any) => (
<AppCard key={app.id} app={app} onRefresh={mutate} />
)))}
<CheckModal />
</nav>
<div ref={anchorRef} className='h-0'> </div>
</>
)
}

View file

@ -15,10 +15,11 @@ import type { AppMode } from '@/types/app'
import { ToastContext } from '@/app/components/base/toast'
import { createApp, fetchAppTemplates } from '@/service/apps'
import AppIcon from '@/app/components/base/app-icon'
import AppsContext from '@/context/app-context'
import AppsContext, { useAppContext } from '@/context/app-context'
import EmojiPicker from '@/app/components/base/emoji-picker'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { AiText } from '@/app/components/base/icons/src/vender/solid/communication'
type NewAppDialogProps = {
show: boolean
@ -29,6 +30,8 @@ type NewAppDialogProps = {
const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
const router = useRouter()
const { notify } = useContext(ToastContext)
const { isCurrentWorkspaceManager } = useAppContext()
const { t } = useTranslation()
const nameInputRef = useRef<HTMLInputElement>(null)
@ -90,7 +93,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
onClose()
notify({ type: 'success', message: t('app.newApp.appCreated') })
mutateApps()
router.push(`/app/${app.id}/overview`)
router.push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
@ -119,13 +122,6 @@ const NewAppDialog = ({ show, onSuccess, 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' 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' placeholder={t('app.appNamePlaceholder') || ''}/>
</div>
<div className='overflow-y-auto'>
<div className={style.newItemCaption}>
<h3 className='inline'>{t('app.newApp.captionAppType')}</h3>
@ -141,29 +137,9 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
</>
)}
</div>
{isWithTemplate
? (
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
{templates?.data?.map((template, index) => (
<li
key={index}
className={classNames(style.listItem, style.selectable, selectedTemplateIndex === index && style.selected)}
onClick={() => setSelectedTemplateIndex(index)}
>
<div className={style.listItemTitle}>
<AppIcon size='small' />
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{template.name}</div>
</div>
</div>
<div className={style.listItemDescription}>{template.model_config?.pre_prompt}</div>
<AppModeLabel mode={template.mode} className='mt-2' />
{/* <AppModeLabel mode='chat' className='mt-2' /> */}
</li>
))}
</ul>
)
: (
{!isWithTemplate && (
(
<>
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
<li
@ -178,10 +154,10 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
<div className={style.listItemHeadingContent}>{t('app.newApp.chatApp')}</div>
</div>
</div>
<div className={style.listItemDescription}>{t('app.newApp.chatAppIntro')}</div>
<div className={classNames(style.listItemFooter, 'justify-end')}>
<div className={`${style.listItemDescription} ${style.noClip}`}>{t('app.newApp.chatAppIntro')}</div>
{/* <div className={classNames(style.listItemFooter, 'justify-end')}>
<a className={style.listItemLink} href='https://udify.app/chat/7CQBa5yyvYLSkZtx' target='_blank'>{t('app.newApp.previewDemo')}<span className={classNames(style.linkIcon, style.grayLinkIcon)} /></a>
</div>
</div> */}
</li>
<li
className={classNames(style.listItem, style.selectable, newAppMode === 'completion' && style.selected)}
@ -189,28 +165,65 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
>
<div className={style.listItemTitle}>
<span className={style.newItemIcon}>
<span className={classNames(style.newItemIconImage, style.newItemIconComplete)} />
{/* <span className={classNames(style.newItemIconImage, style.newItemIconComplete)} /> */}
<AiText className={classNames('w-5 h-5', newAppMode === 'completion' ? 'text-[#155EEF]' : 'text-gray-700')} />
</span>
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{t('app.newApp.completeApp')}</div>
</div>
</div>
<div className={style.listItemDescription}>{t('app.newApp.completeAppIntro')}</div>
<div className={classNames(style.listItemFooter, 'justify-end')}>
<a className={style.listItemLink} href='https://udify.app/completion/aeFTj0VCb3Ok3TUE' target='_blank'>{t('app.newApp.previewDemo')}<span className={classNames(style.linkIcon, style.grayLinkIcon)} /></a>
</div>
<div className={`${style.listItemDescription} ${style.noClip}`}>{t('app.newApp.completeAppIntro')}</div>
</li>
</ul>
<div className='flex items-center h-[34px] mt-2'>
<span
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
onClick={() => setIsWithTemplate(true)}
>
{t('app.newApp.showTemplates')}<span className={style.rightIcon} />
</span>
</div>
</>
)}
)
)}
{isWithTemplate && (
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
{templates?.data?.map((template, index) => (
<li
key={index}
className={classNames(style.listItem, style.selectable, selectedTemplateIndex === index && style.selected)}
onClick={() => setSelectedTemplateIndex(index)}
>
<div className={style.listItemTitle}>
<AppIcon size='small' />
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{template.name}</div>
</div>
</div>
<div className={style.listItemDescription}>{template.model_config?.pre_prompt}</div>
<div className='inline-block pl-3.5'>
<AppModeLabel mode={template.mode} isAgent={template.model_config.agent_mode.enabled} className='mt-2' />
</div>
</li>
))}
</ul>
)}
<div className='mt-8'>
<h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
<div className='flex items-center justify-between gap-3'>
<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' placeholder={t('app.appNamePlaceholder') || ''} />
</div>
</div>
{
!isWithTemplate && (
<div className='flex items-center h-[34px] mt-2'>
<span
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
onClick={() => setIsWithTemplate(true)}
>
{t('app.newApp.showTemplates')}<span className={style.rightIcon} />
</span>
</div>
)
}
</div>
{isAppsFull && <AppsFull loc='app-create' />}
</Dialog>

View file

@ -9,7 +9,7 @@ const AppList = async () => {
const { t } = await translate(locale, 'app')
return (
<div className='flex flex-col overflow-auto bg-gray-100 shrink-0 grow'>
<div className='relative flex flex-col overflow-y-auto bg-gray-100 shrink-0 h-0 grow'>
<Apps />
<footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>

View file

@ -10,19 +10,20 @@
.listItem.selectable {
@apply relative bg-gray-50 outline outline-1 outline-gray-200 -outline-offset-1 shadow-none hover:bg-none hover:shadow-none hover:outline-primary-200 transition-colors;
}
.listItem.selectable * {
@apply relative;
}
.listItem.selectable::before {
content: "";
@apply absolute top-0 left-0 block w-full h-full rounded-lg pointer-events-none opacity-0 transition-opacity duration-200 ease-in-out hover:opacity-100;
background: linear-gradient(
0deg,
background: linear-gradient(0deg,
rgba(235, 245, 255, 0.5),
rgba(235, 245, 255, 0.5)
),
rgba(235, 245, 255, 0.5)),
#ffffff;
}
.listItem.selectable:hover::before {
@apply opacity-100;
}
@ -30,6 +31,7 @@
.listItem.selected {
@apply border-primary-600 hover:border-primary-600 border-2;
}
.listItem.selected::before {
@apply opacity-100;
}
@ -37,9 +39,11 @@
.appIcon {
@apply flex items-center justify-center w-8 h-8 bg-pink-100 rounded-lg grow-0 shrink-0;
}
.appIcon.medium {
@apply w-9 h-9;
}
.appIcon.large {
@apply w-10 h-10;
}
@ -47,34 +51,48 @@
.newItemIcon {
@apply flex items-center justify-center w-8 h-8 transition-colors duration-200 ease-in-out border border-gray-200 rounded-lg hover:bg-white grow-0 shrink-0;
}
.listItem:hover .newItemIcon {
@apply bg-gray-50 border-primary-100;
}
.newItemCard .newItemIcon {
@apply bg-gray-100;
}
.newItemCard:hover .newItemIcon {
@apply bg-white;
}
.selectable .newItemIcon {
@apply bg-gray-50;
}
.selectable:hover .newItemIcon {
@apply bg-primary-50;
}
.newItemIconImage {
@apply grow-0 shrink-0 block w-4 h-4 bg-center bg-contain transition-colors duration-200 ease-in-out;
color: #1f2a37;
}
.listItem:hover .newIconImage {
@apply text-primary-600;
}
.newItemIconAdd {
background-image: url("./apps/assets/add.svg");
}
.newItemIconChat {
background-image: url("./apps/assets/chat.svg");
background-image: url("~@/app/components/base/icons/assets/public/header-nav/studio/Robot.svg");
}
.selected .newItemIconChat {
background-image: url("~@/app/components/base/icons/assets/public/header-nav/studio/Robot-Active.svg");
}
.newItemIconComplete {
background-image: url("./apps/assets/completion.svg");
}
@ -94,14 +112,17 @@
.actionIconWrapper {
@apply hidden h-8 w-8 p-2 rounded-md border-none hover:bg-gray-100 !important;
}
.listItem:hover .actionIconWrapper {
@apply !inline-flex;
}
.deleteDatasetIcon {
@apply hidden grow-0 shrink-0 basis-8 w-8 h-8 rounded-lg transition-colors duration-200 ease-in-out bg-white border border-gray-200 hover:bg-gray-100 bg-center bg-no-repeat;
background-size: 16px;
background-image: url('~@/assets/delete.svg');
}
.listItem:hover .deleteDatasetIcon {
@apply block;
}
@ -110,9 +131,14 @@
@apply mb-3 px-[14px] h-9 text-xs leading-normal text-gray-500 line-clamp-2;
}
.listItemDescription.noClip {
@apply line-clamp-none;
}
.listItemFooter {
@apply flex items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px];
}
.listItemFooter.datasetCardFooter {
@apply flex items-center gap-4 text-xs text-gray-500;
}
@ -124,18 +150,23 @@
.listItemFooterIcon {
@apply block w-3 h-3 bg-center bg-contain;
}
.solidChatIcon {
background-image: url("./apps/assets/chat-solid.svg");
}
.solidCompletionIcon {
background-image: url("./apps/assets/completion-solid.svg");
}
.docIcon {
background-image: url("./datasets/assets/doc.svg");
}
.textIcon {
background-image: url("./datasets/assets/text.svg");
}
.applicationIcon {
background-image: url("./datasets/assets/application.svg");
}
@ -143,6 +174,7 @@
.newItemCardHeading {
@apply transition-colors duration-200 ease-in-out;
}
.listItem:hover .newItemCardHeading {
@apply text-primary-600;
}
@ -150,6 +182,7 @@
.listItemLink {
@apply inline-flex items-center gap-1 text-xs text-gray-400 transition-colors duration-200 ease-in-out;
}
.listItem:hover .listItemLink {
@apply text-primary-600;
}
@ -162,6 +195,7 @@
.linkIcon.grayLinkIcon {
background-image: url("./apps/assets/link-gray.svg");
}
.listItem:hover .grayLinkIcon {
background-image: url("./apps/assets/link.svg");
}
@ -191,6 +225,7 @@
.newItemCaption {
@apply inline-flex items-center mb-2 text-sm font-medium;
}
/* #endregion new app dialog */
.unavailable {
@ -199,4 +234,4 @@
.listItem:hover .unavailable {
@apply opacity-100;
}
}

View file

@ -0,0 +1,10 @@
import React from 'react'
const Custom = () => {
return (
<div>
Custom
</div>
)
}
export default Custom

View file

@ -0,0 +1,23 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import React, { useEffect } from 'react'
import Tools from '@/app/components/tools'
import { LOC } from '@/app/components/tools/types'
const Layout: FC = () => {
const { t } = useTranslation()
useEffect(() => {
document.title = `${t('tools.title')} - Dify`
}, [])
return (
<div className='overflow-hidden' style={{
height: 'calc(100vh - 56px)',
}}>
<Tools loc={LOC.tools} />
</div>
)
}
export default React.memo(Layout)

View file

@ -0,0 +1,10 @@
import React from 'react'
const ThirdPart = () => {
return (
<div>
Third part
</div>
)
}
export default ThirdPart