diff --git a/web/.gitignore b/web/.gitignore index 6942941877..9dd00b1b08 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -47,4 +47,6 @@ package-lock.json .yarnrc.yml # pmpm -pnpm-lock.yaml \ No newline at end of file +pnpm-lock.yaml + +.favorites.json \ No newline at end of file diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx index 8568c7d7b6..14b27f8a64 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx @@ -38,15 +38,24 @@ const AppDetailLayout: FC = (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` diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 97b8f7b64c..17f85d9b61 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -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}
- +
{showConfirmDelete && ( diff --git a/web/app/(commonLayout)/apps/AppModeLabel.tsx b/web/app/(commonLayout)/apps/AppModeLabel.tsx index d40d29a11e..bbe6154bec 100644 --- a/web/app/(commonLayout)/apps/AppModeLabel.tsx +++ b/web/app/(commonLayout)/apps/AppModeLabel.tsx @@ -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 ( - - - {t(`app.modes.${mode}`)} - +
+ { + mode === 'completion' && ( + <> + + {t('app.newApp.completeApp')} + + ) + } + { + mode === 'chat' && !isAgent && ( + <> + + {t('appDebug.assistantType.chatAssistant.name')} + + ) + } + { + mode === 'chat' && isAgent && ( + <> + + {t('appDebug.assistantType.agentAssistant.name')} + + ) + } +
) } diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index d4ea2fef72..c5331d744a 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -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(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 ( - <> -
+ <> +
+
+
+
+ { + handleKeywordsChange(e.target.value) + }} + autoComplete="off" + /> + { + keywords && ( +
+ +
+ ) + } +
+ + +
+ +
) } diff --git a/web/app/(commonLayout)/apps/NewAppDialog.tsx b/web/app/(commonLayout)/apps/NewAppDialog.tsx index 912bcd98dd..f157631de4 100644 --- a/web/app/(commonLayout)/apps/NewAppDialog.tsx +++ b/web/app/(commonLayout)/apps/NewAppDialog.tsx @@ -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(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) => { } > -

{t('app.newApp.captionName')}

- -
- { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> - -
-

{t('app.newApp.captionAppType')}

@@ -141,29 +137,9 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => { )}
- {isWithTemplate - ? ( -
    - {templates?.data?.map((template, index) => ( -
  • setSelectedTemplateIndex(index)} - > -
    - -
    -
    {template.name}
    -
    -
    -
    {template.model_config?.pre_prompt}
    - - {/* */} -
  • - ))} -
- ) - : ( + + {!isWithTemplate && ( + ( <>
  • {
    {t('app.newApp.chatApp')}
-
{t('app.newApp.chatAppIntro')}
-
+
{t('app.newApp.chatAppIntro')}
+ {/* +
*/}
  • { >
    - + {/* */} +
    {t('app.newApp.completeApp')}
    -
    {t('app.newApp.completeAppIntro')}
    - +
    {t('app.newApp.completeAppIntro')}
  • -
    - setIsWithTemplate(true)} - > - {t('app.newApp.showTemplates')} - -
    + - )} + ) + )} + + {isWithTemplate && ( +
      + {templates?.data?.map((template, index) => ( +
    • setSelectedTemplateIndex(index)} + > +
      + +
      +
      {template.name}
      +
      +
      +
      {template.model_config?.pre_prompt}
      +
      + +
      +
    • + ))} +
    + )} + +
    +

    {t('app.newApp.captionName')}

    +
    + { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> + +
    +
    + + { + !isWithTemplate && ( +
    + setIsWithTemplate(true)} + > + {t('app.newApp.showTemplates')} + +
    + ) + } + {isAppsFull && } diff --git a/web/app/(commonLayout)/apps/page.tsx b/web/app/(commonLayout)/apps/page.tsx index e2e774bec7..30c2ef9e57 100644 --- a/web/app/(commonLayout)/apps/page.tsx +++ b/web/app/(commonLayout)/apps/page.tsx @@ -9,7 +9,7 @@ const AppList = async () => { const { t } = await translate(locale, 'app') return ( -
    +