feat: support assistant frontend (#2139)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
This commit is contained in:
parent
e65a2a400d
commit
7bbe12b2bd
194 changed files with 8726 additions and 1586 deletions
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue