From cd1ec65286ae7c896a687638fa9fd8ad21438e21 Mon Sep 17 00:00:00 2001 From: twwu Date: Wed, 16 Jul 2025 16:51:55 +0800 Subject: [PATCH 1/5] feat: convert components to dynamic imports for improved performance --- web/app/(commonLayout)/apps/AppCard.tsx | 26 ++++-- web/app/(commonLayout)/apps/Apps.tsx | 10 +- web/app/(commonLayout)/apps/NewAppCard.tsx | 103 ++++++++++++--------- web/context/modal-context.tsx | 50 +++++++--- 4 files changed, 125 insertions(+), 64 deletions(-) diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index e04c3fdea6..bdb9f3abe4 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -7,10 +7,8 @@ import { useTranslation } from 'react-i18next' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react' import cn from '@/utils/classnames' import type { App } from '@/types/app' -import Confirm from '@/app/components/base/confirm' import Toast, { ToastContext } from '@/app/components/base/toast' import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' -import DuplicateAppModal from '@/app/components/app/duplicate-modal' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' import AppIcon from '@/app/components/base/app-icon' import AppsContext, { useAppContext } from '@/context/app-context' @@ -22,21 +20,37 @@ import { getRedirection } from '@/utils/app-redirection' import { useProviderContext } from '@/context/provider-context' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' -import EditAppModal from '@/app/components/explore/create-app-modal' -import SwitchAppModal from '@/app/components/app/switch-app-modal' import type { Tag } from '@/app/components/base/tag-management/constant' import TagSelector from '@/app/components/base/tag-management/selector' import type { EnvironmentVariable } from '@/app/components/workflow/types' -import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' import { fetchWorkflowDraft } from '@/service/workflow' import { fetchInstalledAppList } from '@/service/explore' import { AppTypeIcon } from '@/app/components/app/type-selector' import Tooltip from '@/app/components/base/tooltip' -import AccessControl from '@/app/components/app/app-access-control' import { AccessMode } from '@/models/access-control' import { useGlobalPublicStore } from '@/context/global-public-context' import { formatTime } from '@/utils/time' import { useGetUserCanAccessApp } from '@/service/access-control' +import dynamic from 'next/dynamic' + +const EditAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), { + ssr: false, +}) +const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-modal'), { + ssr: false, +}) +const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), { + ssr: false, +}) +const Confirm = dynamic(() => import('@/app/components/base/confirm'), { + ssr: false, +}) +const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { + ssr: false, +}) +const AccessControl = dynamic(() => import('@/app/components/app/app-access-control'), { + ssr: false, +}) export type AppCardProps = { app: App diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index 2aa192fb02..9dc8c16097 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -28,10 +28,16 @@ import TabSliderNew from '@/app/components/base/tab-slider-new' import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import Input from '@/app/components/base/input' import { useStore as useTagStore } from '@/app/components/base/tag-management/store' -import TagManagementModal from '@/app/components/base/tag-management' import TagFilter from '@/app/components/base/tag-management/filter' import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' -import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal' +import dynamic from 'next/dynamic' + +const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), { + ssr: false, +}) +const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-from-dsl-modal'), { + ssr: false, +}) const getKey = ( pageIndex: number, diff --git a/web/app/(commonLayout)/apps/NewAppCard.tsx b/web/app/(commonLayout)/apps/NewAppCard.tsx index 0b42577ee3..2761a257e9 100644 --- a/web/app/(commonLayout)/apps/NewAppCard.tsx +++ b/web/app/(commonLayout)/apps/NewAppCard.tsx @@ -6,12 +6,21 @@ import { useSearchParams, } from 'next/navigation' import { useTranslation } from 'react-i18next' -import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog' -import CreateAppModal from '@/app/components/app/create-app-modal' -import CreateFromDSLModal, { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal' +import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal' import { useProviderContext } from '@/context/provider-context' import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files' import cn from '@/utils/classnames' +import dynamic from 'next/dynamic' + +const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), { + ssr: false, +}) +const CreateAppTemplateDialog = dynamic(() => import('@/app/components/app/create-app-dialog'), { + ssr: false, +}) +const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-from-dsl-modal'), { + ssr: false, +}) export type CreateAppCardProps = { className?: string @@ -67,48 +76,54 @@ const CreateAppCard = ( - setShowNewAppModal(false)} - onSuccess={() => { - onPlanInfoChanged() - if (onSuccess) - onSuccess() - }} - onCreateFromTemplate={() => { - setShowNewAppTemplateDialog(true) - setShowNewAppModal(false) - }} - /> - setShowNewAppTemplateDialog(false)} - onSuccess={() => { - onPlanInfoChanged() - if (onSuccess) - onSuccess() - }} - onCreateFromBlank={() => { - setShowNewAppModal(true) - setShowNewAppTemplateDialog(false) - }} - /> - { - setShowCreateFromDSLModal(false) + {showNewAppModal && ( + setShowNewAppModal(false)} + onSuccess={() => { + onPlanInfoChanged() + if (onSuccess) + onSuccess() + }} + onCreateFromTemplate={() => { + setShowNewAppTemplateDialog(true) + setShowNewAppModal(false) + }} + /> + )} + {showNewAppTemplateDialog && ( + setShowNewAppTemplateDialog(false)} + onSuccess={() => { + onPlanInfoChanged() + if (onSuccess) + onSuccess() + }} + onCreateFromBlank={() => { + setShowNewAppModal(true) + setShowNewAppTemplateDialog(false) + }} + /> + )} + {showCreateFromDSLModal && ( + { + setShowCreateFromDSLModal(false) - if (dslUrl) - replace('/') - }} - activeTab={activeTab} - dslUrl={dslUrl} - onSuccess={() => { - onPlanInfoChanged() - if (onSuccess) - onSuccess() - }} - /> + if (dslUrl) + replace('/') + }} + activeTab={activeTab} + dslUrl={dslUrl} + onSuccess={() => { + onPlanInfoChanged() + if (onSuccess) + onSuccess() + }} + /> + )} ) } diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index d86590335d..f6425ec11f 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -4,13 +4,6 @@ import type { Dispatch, SetStateAction } from 'react' import { useCallback, useState } from 'react' import { createContext, useContext, useContextSelector } from 'use-context-selector' import { useRouter, useSearchParams } from 'next/navigation' -import AccountSetting from '@/app/components/header/account-setting' -import ApiBasedExtensionModal from '@/app/components/header/account-setting/api-based-extension-page/modal' -import ModerationSettingModal from '@/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal' -import ExternalDataToolModal from '@/app/components/app/configuration/tools/external-data-tool-modal' -import AnnotationFullModal from '@/app/components/billing/annotation-full/modal' -import ModelModal from '@/app/components/header/account-setting/model-provider-page/model-modal' -import ExternalAPIModal from '@/app/components/datasets/external-api/external-api-modal' import type { ConfigurationMethodEnum, CustomConfigurationModelFixedFields, @@ -20,23 +13,56 @@ import type { import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, } from '@/app/education-apply/constants' -import Pricing from '@/app/components/billing/pricing' import type { ModerationConfig, PromptVariable } from '@/models/debug' import type { ApiBasedExtension, ExternalDataTool, } from '@/models/common' import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations' -import ModelLoadBalancingEntryModal from '@/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal' import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal' -import ModelLoadBalancingModal from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal' -import OpeningSettingModal from '@/app/components/base/features/new-feature-panel/conversation-opener/modal' import type { OpeningStatement } from '@/app/components/base/features/types' import type { InputVar } from '@/app/components/workflow/types' import type { UpdatePluginPayload } from '@/app/components/plugins/types' -import UpdatePlugin from '@/app/components/plugins/update-plugin' import { removeSpecificQueryParam } from '@/utils' import { noop } from 'lodash-es' +import dynamic from 'next/dynamic' + +const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), { + ssr: false, +}) +const ApiBasedExtensionModal = dynamic(() => import('@/app/components/header/account-setting/api-based-extension-page/modal'), { + ssr: false, +}) +const ModerationSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal'), { + ssr: false, +}) +const ExternalDataToolModal = dynamic(() => import('@/app/components/app/configuration/tools/external-data-tool-modal'), { + ssr: false, +}) +const Pricing = dynamic(() => import('@/app/components/billing/pricing'), { + ssr: false, +}) +const AnnotationFullModal = dynamic(() => import('@/app/components/billing/annotation-full/modal'), { + ssr: false, +}) +const ModelModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/model-modal'), { + ssr: false, +}) +const ExternalAPIModal = dynamic(() => import('@/app/components/datasets/external-api/external-api-modal'), { + ssr: false, +}) +const ModelLoadBalancingModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'), { + ssr: false, +}) +const ModelLoadBalancingEntryModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal'), { + ssr: false, +}) +const OpeningSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/conversation-opener/modal'), { + ssr: false, +}) +const UpdatePlugin = dynamic(() => import('@/app/components/plugins/update-plugin'), { + ssr: false, +}) export type ModalState = { payload: T From ce619287b3f90d488fb7cb8f82771193b9e59364 Mon Sep 17 00:00:00 2001 From: twwu Date: Wed, 16 Jul 2025 18:34:03 +0800 Subject: [PATCH 2/5] feat(app-publisher): add relative time formatting for timestamps --- web/app/components/app/app-publisher/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 83a7ffd553..ccf6ddbeaf 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -6,6 +6,7 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' import { RiArrowDownSLine, RiArrowRightSLine, @@ -48,6 +49,7 @@ import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/acces import { AccessMode } from '@/models/access-control' import { fetchAppDetail } from '@/service/apps' import { useGlobalPublicStore } from '@/context/global-public-context' +dayjs.extend(relativeTime) export type AppPublisherProps = { disabled?: boolean @@ -116,7 +118,7 @@ const AppPublisher = ({ } }, [appAccessSubjects, appDetail]) const language = useGetLanguage() - const formatTimeFromNow = useCallback((time: number) => { + const formatTimeFromNow = useCallback(async (time: number) => { return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow() }, [language]) From 947dbd8854865e963580747a40bf0ab7508c7d27 Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 17 Jul 2025 14:00:29 +0800 Subject: [PATCH 3/5] chore(apps): move app list components to components folder --- web/app/(commonLayout)/apps/assets/add.svg | 3 - .../(commonLayout)/apps/assets/chat-solid.svg | 4 - web/app/(commonLayout)/apps/assets/chat.svg | 3 - .../apps/assets/completion-solid.svg | 4 - .../(commonLayout)/apps/assets/completion.svg | 3 - .../(commonLayout)/apps/assets/discord.svg | 3 - web/app/(commonLayout)/apps/assets/github.svg | 17 -- .../(commonLayout)/apps/assets/link-gray.svg | 3 - web/app/(commonLayout)/apps/assets/link.svg | 3 - .../apps/assets/right-arrow.svg | 3 - web/app/(commonLayout)/apps/layout.tsx | 12 - web/app/(commonLayout)/apps/page.tsx | 28 +-- web/app/(commonLayout)/list.module.css | 217 ------------------ .../components/app/type-selector/index.tsx | 76 +++--- .../apps/app-card.tsx} | 4 +- web/app/components/apps/empty.tsx | 35 +++ web/app/components/apps/footer.tsx | 46 ++++ .../apps/hooks/use-apps-query-state.ts | 0 .../apps/hooks/use-dsl-drag-drop.ts | 2 +- web/app/components/apps/index.tsx | 22 ++ .../Apps.tsx => components/apps/list.tsx} | 29 +-- .../apps/new-app-card.tsx} | 21 +- web/app/components/base/app-icon/index.tsx | 3 +- web/next.config.js | 5 +- web/package.json | 4 +- web/pnpm-lock.yaml | 114 +++++++++ 26 files changed, 284 insertions(+), 380 deletions(-) delete mode 100644 web/app/(commonLayout)/apps/assets/add.svg delete mode 100644 web/app/(commonLayout)/apps/assets/chat-solid.svg delete mode 100644 web/app/(commonLayout)/apps/assets/chat.svg delete mode 100644 web/app/(commonLayout)/apps/assets/completion-solid.svg delete mode 100644 web/app/(commonLayout)/apps/assets/completion.svg delete mode 100644 web/app/(commonLayout)/apps/assets/discord.svg delete mode 100644 web/app/(commonLayout)/apps/assets/github.svg delete mode 100644 web/app/(commonLayout)/apps/assets/link-gray.svg delete mode 100644 web/app/(commonLayout)/apps/assets/link.svg delete mode 100644 web/app/(commonLayout)/apps/assets/right-arrow.svg delete mode 100644 web/app/(commonLayout)/apps/layout.tsx delete mode 100644 web/app/(commonLayout)/list.module.css rename web/app/{(commonLayout)/apps/AppCard.tsx => components/apps/app-card.tsx} (99%) create mode 100644 web/app/components/apps/empty.tsx create mode 100644 web/app/components/apps/footer.tsx rename web/app/{(commonLayout) => components}/apps/hooks/use-apps-query-state.ts (100%) rename web/app/{(commonLayout) => components}/apps/hooks/use-dsl-drag-drop.ts (97%) create mode 100644 web/app/components/apps/index.tsx rename web/app/{(commonLayout)/apps/Apps.tsx => components/apps/list.tsx} (92%) rename web/app/{(commonLayout)/apps/NewAppCard.tsx => components/apps/new-app-card.tsx} (94%) diff --git a/web/app/(commonLayout)/apps/assets/add.svg b/web/app/(commonLayout)/apps/assets/add.svg deleted file mode 100644 index 9958e855aa..0000000000 --- a/web/app/(commonLayout)/apps/assets/add.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/(commonLayout)/apps/assets/chat-solid.svg b/web/app/(commonLayout)/apps/assets/chat-solid.svg deleted file mode 100644 index a793e982c0..0000000000 --- a/web/app/(commonLayout)/apps/assets/chat-solid.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web/app/(commonLayout)/apps/assets/chat.svg b/web/app/(commonLayout)/apps/assets/chat.svg deleted file mode 100644 index 0971349a53..0000000000 --- a/web/app/(commonLayout)/apps/assets/chat.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/(commonLayout)/apps/assets/completion-solid.svg b/web/app/(commonLayout)/apps/assets/completion-solid.svg deleted file mode 100644 index a9dc7e3dc1..0000000000 --- a/web/app/(commonLayout)/apps/assets/completion-solid.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/web/app/(commonLayout)/apps/assets/completion.svg b/web/app/(commonLayout)/apps/assets/completion.svg deleted file mode 100644 index 34af4417fe..0000000000 --- a/web/app/(commonLayout)/apps/assets/completion.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/(commonLayout)/apps/assets/discord.svg b/web/app/(commonLayout)/apps/assets/discord.svg deleted file mode 100644 index 9f22a1ab59..0000000000 --- a/web/app/(commonLayout)/apps/assets/discord.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/(commonLayout)/apps/assets/github.svg b/web/app/(commonLayout)/apps/assets/github.svg deleted file mode 100644 index f03798b5e1..0000000000 --- a/web/app/(commonLayout)/apps/assets/github.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/web/app/(commonLayout)/apps/assets/link-gray.svg b/web/app/(commonLayout)/apps/assets/link-gray.svg deleted file mode 100644 index a293cfcf53..0000000000 --- a/web/app/(commonLayout)/apps/assets/link-gray.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/(commonLayout)/apps/assets/link.svg b/web/app/(commonLayout)/apps/assets/link.svg deleted file mode 100644 index 2926c28b16..0000000000 --- a/web/app/(commonLayout)/apps/assets/link.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/(commonLayout)/apps/assets/right-arrow.svg b/web/app/(commonLayout)/apps/assets/right-arrow.svg deleted file mode 100644 index a2c1cedf95..0000000000 --- a/web/app/(commonLayout)/apps/assets/right-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/web/app/(commonLayout)/apps/layout.tsx b/web/app/(commonLayout)/apps/layout.tsx deleted file mode 100644 index 10d04a4188..0000000000 --- a/web/app/(commonLayout)/apps/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -'use client' - -import useDocumentTitle from '@/hooks/use-document-title' -import { useTranslation } from 'react-i18next' - -export default function DatasetsLayout({ children }: { children: React.ReactNode }) { - const { t } = useTranslation() - useDocumentTitle(t('common.menus.apps')) - return (<> - {children} - ) -} diff --git a/web/app/(commonLayout)/apps/page.tsx b/web/app/(commonLayout)/apps/page.tsx index 3f617d41c9..25b6d55d11 100644 --- a/web/app/(commonLayout)/apps/page.tsx +++ b/web/app/(commonLayout)/apps/page.tsx @@ -1,32 +1,8 @@ -'use client' -import { useTranslation } from 'react-i18next' -import { RiDiscordFill, RiGithubFill } from '@remixicon/react' -import Link from 'next/link' -import style from '../list.module.css' -import Apps from './Apps' -import { useEducationInit } from '@/app/education-apply/hooks' -import { useGlobalPublicStore } from '@/context/global-public-context' +import Apps from '@/app/components/apps' const AppList = () => { - const { t } = useTranslation() - useEducationInit() - const { systemFeatures } = useGlobalPublicStore() return ( -
- - {!systemFeatures.branding.enabled &&
-

{t('app.join')}

-

{t('app.communityIntro')}

-
- - - - - - -
-
} -
+ ) } diff --git a/web/app/(commonLayout)/list.module.css b/web/app/(commonLayout)/list.module.css deleted file mode 100644 index c4d3aec29f..0000000000 --- a/web/app/(commonLayout)/list.module.css +++ /dev/null @@ -1,217 +0,0 @@ -.listItem { - @apply col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-xs min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg; -} - -.listItem.newItemCard { - @apply outline outline-1 outline-gray-200 -outline-offset-1 hover:shadow-sm hover:bg-white; - background-color: rgba(229, 231, 235, 0.5); -} - -.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, - rgba(235, 245, 255, 0.5), - rgba(235, 245, 255, 0.5)), - #ffffff; -} - -.listItem.selectable:hover::before { - @apply opacity-100; -} - -.listItem.selected { - @apply border-primary-600 hover:border-primary-600 border-2; -} - -.listItem.selected::before { - @apply opacity-100; -} - -.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; -} - -.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("~@/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"); -} - -.listItemTitle { - @apply flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0; -} - -.listItemHeading { - @apply relative h-8 text-sm font-medium leading-8 grow; -} - -.listItemHeadingContent { - @apply absolute top-0 left-0 w-full h-full overflow-hidden text-ellipsis whitespace-nowrap; -} - -.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; -} - -.listItemDescription { - @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; -} - -.listItemStats { - @apply flex items-center gap-1; -} - -.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"); -} - -.newItemCardHeading { - @apply transition-colors duration-200 ease-in-out; -} - -.listItem:hover .newItemCardHeading { - @apply text-primary-600; -} - -.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; -} - -.linkIcon { - @apply block w-[13px] h-[13px] bg-center bg-contain; - background-image: url("./apps/assets/link.svg"); -} - -.linkIcon.grayLinkIcon { - background-image: url("./apps/assets/link-gray.svg"); -} - -.listItem:hover .grayLinkIcon { - background-image: url("./apps/assets/link.svg"); -} - -.rightIcon { - @apply block w-[13px] h-[13px] bg-center bg-contain; - background-image: url("./apps/assets/right-arrow.svg"); -} - -.socialMediaLink { - @apply flex items-center justify-center w-8 h-8 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out; -} - -.socialMediaIcon { - @apply block w-6 h-6 bg-center bg-contain; -} - -/* #region new app dialog */ -.newItemCaption { - @apply inline-flex items-center mb-2 text-sm font-medium; -} - -/* #endregion new app dialog */ - -.unavailable { - @apply opacity-50; -} - -.listItem:hover .unavailable { - @apply opacity-100; -} diff --git a/web/app/components/app/type-selector/index.tsx b/web/app/components/app/type-selector/index.tsx index a57bac20db..99a76d7ac7 100644 --- a/web/app/components/app/type-selector/index.tsx +++ b/web/app/components/app/type-selector/index.tsx @@ -65,6 +65,44 @@ const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => { export default AppTypeSelector +type AppTypeIconProps = { + type: AppMode + style?: React.CSSProperties + className?: string + wrapperClassName?: string +} + +export const AppTypeIcon = React.memo(({ type, className, wrapperClassName, style }: AppTypeIconProps) => { + const wrapperClassNames = cn('inline-flex h-5 w-5 items-center justify-center rounded-md border border-divider-regular', wrapperClassName) + const iconClassNames = cn('h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100', className) + if (type === 'chat') { + return
+ +
+ } + if (type === 'agent-chat') { + return
+ +
+ } + if (type === 'advanced-chat') { + return
+ +
+ } + if (type === 'workflow') { + return
+ +
+ } + if (type === 'completion') { + return
+ +
+ } + return null +}) + function AppTypeSelectTrigger({ values }: { values: AppSelectorProps['value'] }) { const { t } = useTranslation() if (!values || values.length === 0) { @@ -108,44 +146,6 @@ function AppTypeSelectorItem({ checked, type, onClick }: AppTypeSelectorItemProp } -type AppTypeIconProps = { - type: AppMode - style?: React.CSSProperties - className?: string - wrapperClassName?: string -} - -export function AppTypeIcon({ type, className, wrapperClassName, style }: AppTypeIconProps) { - const wrapperClassNames = cn('inline-flex h-5 w-5 items-center justify-center rounded-md border border-divider-regular', wrapperClassName) - const iconClassNames = cn('h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100', className) - if (type === 'chat') { - return
- -
- } - if (type === 'agent-chat') { - return
- -
- } - if (type === 'advanced-chat') { - return
- -
- } - if (type === 'workflow') { - return
- -
- } - if (type === 'completion') { - return
- -
- } - return null -} - type AppTypeLabelProps = { type: AppMode className?: string diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/components/apps/app-card.tsx similarity index 99% rename from web/app/(commonLayout)/apps/AppCard.tsx rename to web/app/components/apps/app-card.tsx index bdb9f3abe4..5bbd8d17f6 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/components/apps/app-card.tsx @@ -1,8 +1,8 @@ 'use client' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useContext, useContextSelector } from 'use-context-selector' import { useRouter } from 'next/navigation' -import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react' import cn from '@/utils/classnames' @@ -497,4 +497,4 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { ) } -export default AppCard +export default React.memo(AppCard) diff --git a/web/app/components/apps/empty.tsx b/web/app/components/apps/empty.tsx new file mode 100644 index 0000000000..e6b52294a2 --- /dev/null +++ b/web/app/components/apps/empty.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' + +const DefaultCards = React.memo(() => { + const renderArray = Array.from({ length: 36 }) + return ( + <> + { + renderArray.map((_, index) => ( +
+ )) + } + + ) +}) + +const Empty = () => { + const { t } = useTranslation() + + return ( + <> + +
+ + {t('app.newApp.noAppsFound')} + +
+ + ) +} + +export default React.memo(Empty) diff --git a/web/app/components/apps/footer.tsx b/web/app/components/apps/footer.tsx new file mode 100644 index 0000000000..7bee272342 --- /dev/null +++ b/web/app/components/apps/footer.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import Link from 'next/link' +import { RiDiscordFill, RiGithubFill } from '@remixicon/react' +import { useTranslation } from 'react-i18next' + +type CustomLinkProps = { + href: string + children: React.ReactNode +} + +const CustomLink = React.memo(({ + href, + children, +}: CustomLinkProps) => { + return ( + + {children} + + ) +}) + +const Footer = () => { + const { t } = useTranslation() + + return ( +
+

{t('app.join')}

+

{t('app.communityIntro')}

+
+ + + + + + +
+
+ ) +} + +export default React.memo(Footer) diff --git a/web/app/(commonLayout)/apps/hooks/use-apps-query-state.ts b/web/app/components/apps/hooks/use-apps-query-state.ts similarity index 100% rename from web/app/(commonLayout)/apps/hooks/use-apps-query-state.ts rename to web/app/components/apps/hooks/use-apps-query-state.ts diff --git a/web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts b/web/app/components/apps/hooks/use-dsl-drag-drop.ts similarity index 97% rename from web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts rename to web/app/components/apps/hooks/use-dsl-drag-drop.ts index 96942ec54e..dda5773062 100644 --- a/web/app/(commonLayout)/apps/hooks/use-dsl-drag-drop.ts +++ b/web/app/components/apps/hooks/use-dsl-drag-drop.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' type DSLDragDropHookProps = { onDSLFileDropped: (file: File) => void - containerRef: React.RefObject + containerRef: React.RefObject enabled?: boolean } diff --git a/web/app/components/apps/index.tsx b/web/app/components/apps/index.tsx new file mode 100644 index 0000000000..68748439c7 --- /dev/null +++ b/web/app/components/apps/index.tsx @@ -0,0 +1,22 @@ +'use client' +import { useEducationInit } from '@/app/education-apply/hooks' +import { useGlobalPublicStore } from '@/context/global-public-context' +import List from './list' +import Footer from './footer' + +const Apps = () => { + const { systemFeatures } = useGlobalPublicStore() + + useEducationInit() + + return ( +
+ + {!systemFeatures.branding.enabled && ( +
+ )} +
+ ) +} + +export default Apps diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/components/apps/list.tsx similarity index 92% rename from web/app/(commonLayout)/apps/Apps.tsx rename to web/app/components/apps/list.tsx index 9dc8c16097..359eaeabd4 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/components/apps/list.tsx @@ -15,8 +15,8 @@ import { RiMessage3Line, RiRobot3Line, } from '@remixicon/react' -import AppCard from './AppCard' -import NewAppCard from './NewAppCard' +import AppCard from './app-card' +import NewAppCard from './new-app-card' import useAppsQueryState from './hooks/use-apps-query-state' import { useDSLDragDrop } from './hooks/use-dsl-drag-drop' import type { AppListResponse } from '@/models/app' @@ -31,6 +31,7 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st import TagFilter from '@/app/components/base/tag-management/filter' import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' import dynamic from 'next/dynamic' +import Empty from './empty' const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), { ssr: false, @@ -63,7 +64,7 @@ const getKey = ( return null } -const Apps = () => { +const List = () => { const { t } = useTranslation() const router = useRouter() const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() @@ -215,7 +216,7 @@ const Apps = () => { :
{isCurrentWorkspaceEditor && } - +
} {isCurrentWorkspaceEditor && ( @@ -254,22 +255,4 @@ const Apps = () => { ) } -export default Apps - -function NoAppsFound() { - const { t } = useTranslation() - function renderDefaultCard() { - const defaultCards = Array.from({ length: 36 }, (_, index) => ( -
- )) - return defaultCards - } - return ( - <> - {renderDefaultCard()} -
- {t('app.newApp.noAppsFound')} -
- - ) -} +export default List diff --git a/web/app/(commonLayout)/apps/NewAppCard.tsx b/web/app/components/apps/new-app-card.tsx similarity index 94% rename from web/app/(commonLayout)/apps/NewAppCard.tsx rename to web/app/components/apps/new-app-card.tsx index 2761a257e9..451d2ae326 100644 --- a/web/app/(commonLayout)/apps/NewAppCard.tsx +++ b/web/app/components/apps/new-app-card.tsx @@ -1,6 +1,6 @@ 'use client' -import { useMemo, useState } from 'react' +import React, { useMemo, useState } from 'react' import { useRouter, useSearchParams, @@ -25,17 +25,14 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro export type CreateAppCardProps = { className?: string onSuccess?: () => void + ref: React.RefObject } -const CreateAppCard = ( - { - ref, - className, - onSuccess, - }: CreateAppCardProps & { - ref: React.RefObject; - }, -) => { +const CreateAppCard = ({ + ref, + className, + onSuccess, +}: CreateAppCardProps) => { const { t } = useTranslation() const { onPlanInfoChanged } = useProviderContext() const searchParams = useSearchParams() @@ -129,5 +126,5 @@ const CreateAppCard = ( } CreateAppCard.displayName = 'CreateAppCard' -export default CreateAppCard -export { CreateAppCard } + +export default React.memo(CreateAppCard) diff --git a/web/app/components/base/app-icon/index.tsx b/web/app/components/base/app-icon/index.tsx index 003d929c8c..b4724ca5de 100644 --- a/web/app/components/base/app-icon/index.tsx +++ b/web/app/components/base/app-icon/index.tsx @@ -1,5 +1,6 @@ 'use client' +import React from 'react' import type { FC } from 'react' import { init } from 'emoji-mart' import data from '@emoji-mart/data' @@ -71,4 +72,4 @@ const AppIcon: FC = ({ } -export default AppIcon +export default React.memo(AppIcon) diff --git a/web/next.config.js b/web/next.config.js index 9ce1b35644..00793bf26a 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -12,6 +12,9 @@ const withMDX = require('@next/mdx')({ // providerImportSource: "@mdx-js/react", }, }) +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}) // the default url to prevent parse url error when running jest const hasSetWebPrefix = process.env.NEXT_PUBLIC_WEB_PREFIX @@ -66,4 +69,4 @@ const nextConfig = { output: 'standalone', } -module.exports = withMDX(nextConfig) +module.exports = withBundleAnalyzer(withMDX(nextConfig)) diff --git a/web/package.json b/web/package.json index 9099d3ed36..b9577c6e55 100644 --- a/web/package.json +++ b/web/package.json @@ -36,7 +36,8 @@ "test:watch": "jest --watch", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", - "preinstall": "npx only-allow pnpm" + "preinstall": "npx only-allow pnpm", + "analyze": "ANALYZE=true pnpm build" }, "dependencies": { "@babel/runtime": "^7.22.3", @@ -58,6 +59,7 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@monaco-editor/react": "^4.6.0", + "@next/bundle-analyzer": "^15.4.1", "@next/mdx": "~15.3.5", "@octokit/core": "^6.1.2", "@octokit/request-error": "^6.1.5", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 746b2b3e72..4da2578bdb 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@monaco-editor/react': specifier: ^4.6.0 version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@next/bundle-analyzer': + specifier: ^15.4.1 + version: 15.4.1 '@next/mdx': specifier: ~15.3.5 version: 15.3.5(@mdx-js/loader@3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)))(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)) @@ -1277,6 +1280,10 @@ packages: resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} engines: {node: '>17.0.0'} + '@discoveryjs/json-ext@0.5.7': + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + '@emnapi/core@1.4.0': resolution: {integrity: sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==} @@ -2224,6 +2231,9 @@ packages: '@napi-rs/wasm-runtime@0.2.8': resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==} + '@next/bundle-analyzer@15.4.1': + resolution: {integrity: sha512-O5R3iPLR3/oQWFIXl+Mnd02IyhvWBterTlXcceIGw29QHWL/gjvyO0eIVEvrJPS7zzE6/NSu1TiSVgi8mxotlw==} + '@next/env@15.3.5': resolution: {integrity: sha512-7g06v8BUVtN2njAX/r8gheoVffhiKFVt4nx74Tt6G4Hqw9HCLYQVx/GkH2qHvPtAHZaUNZ0VXAa0pQP6v1wk7g==} @@ -2455,6 +2465,9 @@ packages: webpack-plugin-serve: optional: true + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@react-aria/focus@3.20.1': resolution: {integrity: sha512-lgYs+sQ1TtBrAXnAdRBQrBo0/7o5H6IrfDxec1j+VRpcXL0xyk0xPq+m3lZp8typzIghqDgpnKkJ5Jf4OrzPIw==} peerDependencies: @@ -4561,6 +4574,9 @@ packages: dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -4734,6 +4750,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + echarts-for-react@3.0.2: resolution: {integrity: sha512-DRwIiTzx8JfwPOVgGttDytBqdp5VzCSyMRIxubgU/g2n9y3VLUmF2FK7Icmg/sNVkv4+rktmrLN9w22U2yy3fA==} peerDependencies: @@ -5551,6 +5570,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -5955,6 +5978,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -6739,6 +6766,10 @@ packages: monaco-editor@0.52.2: resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -6925,6 +6956,10 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -7919,6 +7954,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -8280,6 +8319,10 @@ packages: resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -8707,6 +8750,11 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-bundle-analyzer@4.10.1: + resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} + engines: {node: '>= 10.13.0'} + hasBin: true + webpack-code-inspector-plugin@0.18.3: resolution: {integrity: sha512-3782rsJhBnRiw0IpR6EqnyGDQoiSq0CcGeLJ52rZXlszYCe8igXtcujq7OhI0byaivWQ1LW7sXKyMEoVpBhq0w==} @@ -8798,6 +8846,18 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.1: resolution: {integrity: sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==} engines: {node: '>=10.0.0'} @@ -9817,6 +9877,8 @@ snapshots: '@dagrejs/graphlib@2.2.4': {} + '@discoveryjs/json-ext@0.5.7': {} + '@emnapi/core@1.4.0': dependencies: '@emnapi/wasi-threads': 1.0.1 @@ -10883,6 +10945,13 @@ snapshots: '@tybys/wasm-util': 0.9.0 optional: true + '@next/bundle-analyzer@15.4.1': + dependencies: + webpack-bundle-analyzer: 4.10.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@next/env@15.3.5': {} '@next/eslint-plugin-next@15.3.5': @@ -11058,6 +11127,8 @@ snapshots: type-fest: 4.39.1 webpack-hot-middleware: 2.26.1 + '@polka/url@1.0.0-next.29': {} + '@react-aria/focus@3.20.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@react-aria/interactions': 3.24.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -13608,6 +13679,8 @@ snapshots: dayjs@1.11.13: {} + debounce@1.2.1: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -13756,6 +13829,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer@0.1.2: {} + echarts-for-react@3.0.2(echarts@5.6.0)(react@19.1.0): dependencies: echarts: 5.6.0 @@ -14961,6 +15036,10 @@ snapshots: graphemer@1.4.0: {} + gzip-size@6.0.0: + dependencies: + duplexer: 0.1.2 + hachure-fill@0.5.2: {} happy-dom@17.4.4: @@ -15435,6 +15514,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -16699,6 +16780,8 @@ snapshots: monaco-editor@0.52.2: {} + mrmime@2.0.1: {} + ms@2.1.3: {} mz@2.7.0: @@ -16911,6 +16994,8 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + opener@1.5.2: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -18132,6 +18217,12 @@ snapshots: dependencies: is-arrayish: 0.3.2 + sirv@2.0.4: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} size-sensor@1.0.2: {} @@ -18517,6 +18608,8 @@ snapshots: dependencies: eslint-visitor-keys: 3.4.3 + totalist@3.0.1: {} + tr46@0.0.3: optional: true @@ -18971,6 +19064,25 @@ snapshots: webidl-conversions@7.0.0: {} + webpack-bundle-analyzer@4.10.1: + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.14.1 + acorn-walk: 8.3.4 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + is-plain-object: 5.0.0 + opener: 1.5.2 + picocolors: 1.1.1 + sirv: 2.0.4 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + webpack-code-inspector-plugin@0.18.3: dependencies: code-inspector-core: 0.18.3 @@ -19117,6 +19229,8 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + ws@7.5.10: {} + ws@8.18.1: {} xml-name-validator@4.0.0: {} From 1df1ffa2ec798f55cee947d7acf8256158dbdd7d Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 17 Jul 2025 14:08:00 +0800 Subject: [PATCH 4/5] fix(apps): add translation and document title for Apps component --- web/app/components/apps/index.tsx | 4 ++++ web/package.json | 2 +- web/pnpm-lock.yaml | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/web/app/components/apps/index.tsx b/web/app/components/apps/index.tsx index 68748439c7..be81a77dc3 100644 --- a/web/app/components/apps/index.tsx +++ b/web/app/components/apps/index.tsx @@ -3,10 +3,14 @@ import { useEducationInit } from '@/app/education-apply/hooks' import { useGlobalPublicStore } from '@/context/global-public-context' import List from './list' import Footer from './footer' +import useDocumentTitle from '@/hooks/use-document-title' +import { useTranslation } from 'react-i18next' const Apps = () => { + const { t } = useTranslation() const { systemFeatures } = useGlobalPublicStore() + useDocumentTitle(t('common.menus.apps')) useEducationInit() return ( diff --git a/web/package.json b/web/package.json index b9577c6e55..d369cc9f6c 100644 --- a/web/package.json +++ b/web/package.json @@ -59,7 +59,6 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@monaco-editor/react": "^4.6.0", - "@next/bundle-analyzer": "^15.4.1", "@next/mdx": "~15.3.5", "@octokit/core": "^6.1.2", "@octokit/request-error": "^6.1.5", @@ -161,6 +160,7 @@ "@eslint/js": "^9.20.0", "@faker-js/faker": "^9.0.3", "@happy-dom/jest-environment": "^17.4.4", + "@next/bundle-analyzer": "^15.4.1", "@next/eslint-plugin-next": "~15.3.5", "@rgrove/parse-xml": "^4.1.0", "@storybook/addon-essentials": "8.5.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 4da2578bdb..659e8ba144 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -75,9 +75,6 @@ importers: '@monaco-editor/react': specifier: ^4.6.0 version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@next/bundle-analyzer': - specifier: ^15.4.1 - version: 15.4.1 '@next/mdx': specifier: ~15.3.5 version: 15.3.5(@mdx-js/loader@3.1.0(acorn@8.14.1)(webpack@5.99.5(esbuild@0.25.0)(uglify-js@3.19.3)))(@mdx-js/react@3.1.0(@types/react@19.1.8)(react@19.1.0)) @@ -376,6 +373,9 @@ importers: '@happy-dom/jest-environment': specifier: ^17.4.4 version: 17.4.4 + '@next/bundle-analyzer': + specifier: ^15.4.1 + version: 15.4.1 '@next/eslint-plugin-next': specifier: ~15.3.5 version: 15.3.5 From d34c95bf8eb5ce9963ee56bb9f2c8dc1063df5cd Mon Sep 17 00:00:00 2001 From: twwu Date: Thu, 17 Jul 2025 15:32:42 +0800 Subject: [PATCH 5/5] feat: convert components to dynamic imports for improved performance --- .../[appId]/workflow/page.tsx | 2 -- web/app/components/app-sidebar/app-info.tsx | 26 ++++++++++---- .../components/app/app-publisher/index.tsx | 6 ++-- .../app/app-publisher/suggested-action.tsx | 4 +-- .../components/workflow-children.tsx | 14 ++++++-- .../components/workflow-panel.tsx | 34 ++++++++++++++----- web/app/components/workflow-app/index.tsx | 2 ++ .../workflow/header/header-in-normal.tsx | 3 +- web/app/components/workflow/header/index.tsx | 11 ++++-- web/app/components/workflow/index.tsx | 6 +++- 10 files changed, 79 insertions(+), 29 deletions(-) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx index d5df70f004..15da0bbed2 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx @@ -1,5 +1,3 @@ -'use client' - import WorkflowApp from '@/app/components/workflow-app' const Page = () => { diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index d5a04ec420..e85eaa2f53 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -12,23 +12,17 @@ import { RiFileUploadLine, } from '@remixicon/react' import AppIcon from '../base/app-icon' -import SwitchAppModal from '../app/switch-app-modal' import cn from '@/utils/classnames' -import Confirm from '@/app/components/base/confirm' import { useStore as useAppStore } from '@/app/components/app/store' import { ToastContext } from '@/app/components/base/toast' import AppsContext, { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' -import DuplicateAppModal from '@/app/components/app/duplicate-modal' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' -import CreateAppModal from '@/app/components/explore/create-app-modal' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { getRedirection } from '@/utils/app-redirection' -import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' -import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' import { fetchWorkflowDraft } from '@/service/workflow' import ContentDialog from '@/app/components/base/content-dialog' import Button from '@/app/components/base/button' @@ -36,6 +30,26 @@ import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overvie import Divider from '../base/divider' import type { Operation } from './app-operations' import AppOperations from './app-operations' +import dynamic from 'next/dynamic' + +const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), { + ssr: false, +}) +const CreateAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), { + ssr: false, +}) +const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-modal'), { + ssr: false, +}) +const Confirm = dynamic(() => import('@/app/components/base/confirm'), { + ssr: false, +}) +const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), { + ssr: false, +}) +const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { + ssr: false, +}) export type IAppInfoProps = { expand: boolean diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index ccf6ddbeaf..cb98aa4950 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -118,7 +118,8 @@ const AppPublisher = ({ } }, [appAccessSubjects, appDetail]) const language = useGetLanguage() - const formatTimeFromNow = useCallback(async (time: number) => { + + const formatTimeFromNow = useCallback((time: number) => { return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow() }, [language]) @@ -182,8 +183,7 @@ const AppPublisher = ({ if (publishDisabled || published) return handlePublish() - }, - { exactMatch: true, useCapture: true }) + }, { exactMatch: true, useCapture: true }) return ( <> diff --git a/web/app/components/app/app-publisher/suggested-action.tsx b/web/app/components/app/app-publisher/suggested-action.tsx index 8d4ab3d39c..2535de6654 100644 --- a/web/app/components/app/app-publisher/suggested-action.tsx +++ b/web/app/components/app/app-publisher/suggested-action.tsx @@ -20,8 +20,8 @@ const SuggestedAction = ({ icon, link, disabled, children, className, onClick, . target='_blank' rel='noreferrer' className={classNames( - 'flex justify-start items-center gap-2 py-2 px-2.5 bg-background-section-burn rounded-lg text-text-secondary transition-colors [&:not(:first-child)]:mt-1', - disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'text-text-secondary hover:bg-state-accent-hover hover:text-text-accent cursor-pointer', + 'flex items-center justify-start gap-2 rounded-lg bg-background-section-burn px-2.5 py-2 text-text-secondary transition-colors [&:not(:first-child)]:mt-1', + disabled ? 'cursor-not-allowed opacity-30 shadow-xs' : 'cursor-pointer text-text-secondary hover:bg-state-accent-hover hover:text-text-accent', className, )} onClick={handleClick} diff --git a/web/app/components/workflow-app/components/workflow-children.tsx b/web/app/components/workflow-app/components/workflow-children.tsx index 6a6bbcd61a..670630e574 100644 --- a/web/app/components/workflow-app/components/workflow-children.tsx +++ b/web/app/components/workflow-app/components/workflow-children.tsx @@ -5,10 +5,7 @@ import { import type { EnvironmentVariable } from '@/app/components/workflow/types' import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants' import { useStore } from '@/app/components/workflow/store' -import Features from '@/app/components/workflow/features' import PluginDependency from '@/app/components/workflow/plugin-dependency' -import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal' -import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' import { useDSL, usePanelInteractions, @@ -16,6 +13,17 @@ import { import { useEventEmitterContextContext } from '@/context/event-emitter' import WorkflowHeader from './workflow-header' import WorkflowPanel from './workflow-panel' +import dynamic from 'next/dynamic' + +const Features = dynamic(() => import('@/app/components/workflow/features'), { + ssr: false, +}) +const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), { + ssr: false, +}) +const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { + ssr: false, +}) const WorkflowChildren = () => { const { eventEmitter } = useEventEmitterContextContext() diff --git a/web/app/components/workflow-app/components/workflow-panel.tsx b/web/app/components/workflow-app/components/workflow-panel.tsx index dd368660ce..013f2834ef 100644 --- a/web/app/components/workflow-app/components/workflow-panel.tsx +++ b/web/app/components/workflow-app/components/workflow-panel.tsx @@ -4,17 +4,35 @@ import { useStore } from '@/app/components/workflow/store' import { useIsChatMode, } from '../hooks' -import DebugAndPreview from '@/app/components/workflow/panel/debug-and-preview' -import Record from '@/app/components/workflow/panel/record' -import WorkflowPreview from '@/app/components/workflow/panel/workflow-preview' -import ChatRecord from '@/app/components/workflow/panel/chat-record' -import ChatVariablePanel from '@/app/components/workflow/panel/chat-variable-panel' -import GlobalVariablePanel from '@/app/components/workflow/panel/global-variable-panel' -import VersionHistoryPanel from '@/app/components/workflow/panel/version-history-panel' import { useStore as useAppStore } from '@/app/components/app/store' -import MessageLogModal from '@/app/components/base/message-log-modal' import type { PanelProps } from '@/app/components/workflow/panel' import Panel from '@/app/components/workflow/panel' +import dynamic from 'next/dynamic' + +const MessageLogModal = dynamic(() => import('@/app/components/base/message-log-modal'), { + ssr: false, +}) +const Record = dynamic(() => import('@/app/components/workflow/panel/record'), { + ssr: false, +}) +const ChatRecord = dynamic(() => import('@/app/components/workflow/panel/chat-record'), { + ssr: false, +}) +const DebugAndPreview = dynamic(() => import('@/app/components/workflow/panel/debug-and-preview'), { + ssr: false, +}) +const WorkflowPreview = dynamic(() => import('@/app/components/workflow/panel/workflow-preview'), { + ssr: false, +}) +const ChatVariablePanel = dynamic(() => import('@/app/components/workflow/panel/chat-variable-panel'), { + ssr: false, +}) +const GlobalVariablePanel = dynamic(() => import('@/app/components/workflow/panel/global-variable-panel'), { + ssr: false, +}) +const VersionHistoryPanel = dynamic(() => import('@/app/components/workflow/panel/version-history-panel'), { + ssr: false, +}) const WorkflowPanelOnLeft = () => { const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index 761a7f29c4..471d4de0d8 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -1,3 +1,5 @@ +'use client' + import { useMemo, } from 'react' diff --git a/web/app/components/workflow/header/header-in-normal.tsx b/web/app/components/workflow/header/header-in-normal.tsx index 5768e6bc06..79a6509a7a 100644 --- a/web/app/components/workflow/header/header-in-normal.tsx +++ b/web/app/components/workflow/header/header-in-normal.tsx @@ -50,8 +50,7 @@ const HeaderInNormal = ({ setShowDebugAndPreviewPanel(false) setShowVariableInspectPanel(false) setShowChatVariablePanel(false) - }, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode, - setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel]) + }, [workflowStore, handleBackupDraft, selectedNode, handleNodeSelect, setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel, setShowChatVariablePanel]) return ( <> diff --git a/web/app/components/workflow/header/index.tsx b/web/app/components/workflow/header/index.tsx index 7713753478..8f6f8204df 100644 --- a/web/app/components/workflow/header/index.tsx +++ b/web/app/components/workflow/header/index.tsx @@ -4,10 +4,17 @@ import { } from '../hooks' import type { HeaderInNormalProps } from './header-in-normal' import HeaderInNormal from './header-in-normal' -import HeaderInHistory from './header-in-view-history' import type { HeaderInRestoringProps } from './header-in-restoring' -import HeaderInRestoring from './header-in-restoring' import { useStore } from '../store' +import dynamic from 'next/dynamic' + +const HeaderInHistory = dynamic(() => import('./header-in-view-history'), { + ssr: false, +}) +const HeaderInRestoring = dynamic(() => import('./header-in-restoring'), { + ssr: false, +}) + export type HeaderProps = { normal?: HeaderInNormalProps restoring?: HeaderInRestoringProps diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 8ea861ebb4..5db25d0676 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -79,10 +79,14 @@ import { } from './constants' import { WorkflowHistoryProvider } from './workflow-history-store' import { useEventEmitterContextContext } from '@/context/event-emitter' -import Confirm from '@/app/components/base/confirm' import DatasetsDetailProvider from './datasets-detail-store/provider' import { HooksStoreContextProvider } from './hooks-store' import type { Shape as HooksStoreShape } from './hooks-store' +import dynamic from 'next/dynamic' + +const Confirm = dynamic(() => import('@/app/components/base/confirm'), { + ssr: false, +}) const nodeTypes = { [CUSTOM_NODE]: CustomNode,