From 33b3eaf324127b94779546fd4fa7c70a3c755414 Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 25 May 2023 16:59:47 +0800 Subject: [PATCH] Feat/explore (#198) --- web/app/(commonLayout)/apps/Apps.tsx | 8 ++ web/app/(commonLayout)/explore/apps/page.tsx | 8 ++ .../explore/installed/[appId]/page.tsx | 15 ++ web/app/(commonLayout)/explore/layout.tsx | 16 +++ web/app/(shareLayout)/chat/[token]/page.tsx | 6 +- .../app/text-generate/item/index.tsx | 16 ++- web/app/components/explore/app-card/index.tsx | 65 +++++++++ .../explore/app-card/style.module.css | 20 +++ web/app/components/explore/app-list/index.tsx | 130 ++++++++++++++++++ .../explore/app-list/style.module.css | 17 +++ web/app/components/explore/category.tsx | 48 +++++++ .../explore/create-app-modal/index.tsx | 86 ++++++++++++ .../explore/create-app-modal/style.module.css | 36 +++++ web/app/components/explore/index.tsx | 51 +++++++ .../explore/installed-app/index.tsx | 37 +++++ .../explore/item-operation/index.tsx | 60 ++++++++ .../explore/item-operation/style.module.css | 31 +++++ .../explore/sidebar/app-nav-item/index.tsx | 73 ++++++++++ .../sidebar/app-nav-item/style.module.css | 17 +++ web/app/components/explore/sidebar/index.tsx | 128 +++++++++++++++++ web/app/components/header/index.tsx | 21 ++- web/app/components/share/chat/index.tsx | 100 ++++++++++---- .../share/chat/sidebar/app-info/index.tsx | 27 ++++ .../components/share/chat/sidebar/index.tsx | 76 ++++++++-- .../components/share/chat/style.module.css | 3 + .../share/text-generation/index.tsx | 63 +++++++-- .../share/text-generation/style.module.css | 6 + web/config/index.ts | 4 + web/context/explore-context.ts | 20 +++ web/i18n/i18next-config.ts | 4 + web/i18n/lang/common.en.ts | 4 +- web/i18n/lang/common.zh.ts | 4 +- web/i18n/lang/explore.en.ts | 38 +++++ web/i18n/lang/explore.zh.ts | 38 +++++ web/models/explore.ts | 30 ++++ web/service/explore.ts | 33 +++++ web/service/share.ts | 69 ++++++---- web/types/app.ts | 1 + 38 files changed, 1312 insertions(+), 97 deletions(-) create mode 100644 web/app/(commonLayout)/explore/apps/page.tsx create mode 100644 web/app/(commonLayout)/explore/installed/[appId]/page.tsx create mode 100644 web/app/(commonLayout)/explore/layout.tsx create mode 100644 web/app/components/explore/app-card/index.tsx create mode 100644 web/app/components/explore/app-card/style.module.css create mode 100644 web/app/components/explore/app-list/index.tsx create mode 100644 web/app/components/explore/app-list/style.module.css create mode 100644 web/app/components/explore/category.tsx create mode 100644 web/app/components/explore/create-app-modal/index.tsx create mode 100644 web/app/components/explore/create-app-modal/style.module.css create mode 100644 web/app/components/explore/index.tsx create mode 100644 web/app/components/explore/installed-app/index.tsx create mode 100644 web/app/components/explore/item-operation/index.tsx create mode 100644 web/app/components/explore/item-operation/style.module.css create mode 100644 web/app/components/explore/sidebar/app-nav-item/index.tsx create mode 100644 web/app/components/explore/sidebar/app-nav-item/style.module.css create mode 100644 web/app/components/explore/sidebar/index.tsx create mode 100644 web/app/components/share/chat/sidebar/app-info/index.tsx create mode 100644 web/app/components/share/chat/style.module.css create mode 100644 web/context/explore-context.ts create mode 100644 web/i18n/lang/explore.en.ts create mode 100644 web/i18n/lang/explore.zh.ts create mode 100644 web/models/explore.ts create mode 100644 web/service/explore.ts diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index 11da845165..13f983acec 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -8,6 +8,7 @@ import NewAppCard from './NewAppCard' import { AppListResponse } from '@/models/app' import { fetchAppList } from '@/service/apps' import { useSelector } from '@/context/app-context' +import { NEED_REFRESH_APP_LIST_KEY } from '@/config' const getKey = (pageIndex: number, previousPageData: AppListResponse) => { if (!pageIndex || previousPageData.has_more) @@ -21,6 +22,13 @@ const Apps = () => { const pageContainerRef = useSelector(state => state.pageContainerRef) const anchorRef = useRef(null) + useEffect(() => { + if(localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') { + localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) + mutate() + } + }, []) + useEffect(() => { loadingStateRef.current = isLoading }, [isLoading]) diff --git a/web/app/(commonLayout)/explore/apps/page.tsx b/web/app/(commonLayout)/explore/apps/page.tsx new file mode 100644 index 0000000000..8066728562 --- /dev/null +++ b/web/app/(commonLayout)/explore/apps/page.tsx @@ -0,0 +1,8 @@ +import AppList from "@/app/components/explore/app-list" +import React from 'react' + +const Apps = ({ }) => { + return +} + +export default React.memo(Apps) diff --git a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx new file mode 100644 index 0000000000..8a9000108c --- /dev/null +++ b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx @@ -0,0 +1,15 @@ +import React, { FC } from 'react' +import Main from '@/app/components/explore/installed-app' + +export interface IInstalledAppProps { + params: { + appId: string + } +} + +const InstalledApp: FC = ({ params: {appId} }) => { + return ( +
+ ) +} +export default React.memo(InstalledApp) diff --git a/web/app/(commonLayout)/explore/layout.tsx b/web/app/(commonLayout)/explore/layout.tsx new file mode 100644 index 0000000000..3af7bb1e07 --- /dev/null +++ b/web/app/(commonLayout)/explore/layout.tsx @@ -0,0 +1,16 @@ +import type { FC } from 'react' +import React from 'react' +import ExploreClient from '@/app/components/explore' +export type IAppDetail = { + children: React.ReactNode +} + +const AppDetail: FC = ({ children }) => { + return ( + + {children} + + ) +} + +export default React.memo(AppDetail) diff --git a/web/app/(shareLayout)/chat/[token]/page.tsx b/web/app/(shareLayout)/chat/[token]/page.tsx index 472bc36091..abbee6a6f8 100644 --- a/web/app/(shareLayout)/chat/[token]/page.tsx +++ b/web/app/(shareLayout)/chat/[token]/page.tsx @@ -4,12 +4,10 @@ import React from 'react' import type { IMainProps } from '@/app/components/share/chat' import Main from '@/app/components/share/chat' -const Chat: FC = ({ - params, -}: any) => { +const Chat: FC = () => { return ( -
+
) } diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index f63ede0cae..700df81d23 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -9,7 +9,7 @@ import Toast from '@/app/components/base/toast' import { Feedbacktype } from '@/app/components/app/chat' import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline' import { useBoolean } from 'ahooks' -import { fetcMoreLikeThis, updateFeedback } from '@/service/share' +import { fetchMoreLikeThis, updateFeedback } from '@/service/share' const MAX_DEPTH = 3 export interface IGenerationItemProps { @@ -24,6 +24,8 @@ export interface IGenerationItemProps { onFeedback?: (feedback: Feedbacktype) => void onSave?: (messageId: string) => void isMobile?: boolean + isInstalledApp: boolean, + installedAppId?: string, } export const SimpleBtn = ({ className, onClick, children }: { @@ -75,7 +77,9 @@ const GenerationItem: FC = ({ onFeedback, onSave, depth = 1, - isMobile + isMobile, + isInstalledApp, + installedAppId, }) => { const { t } = useTranslation() const isTop = depth === 1 @@ -88,7 +92,7 @@ const GenerationItem: FC = ({ }) const handleFeedback = async (childFeedback: Feedbacktype) => { - await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }) + await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId) setChildFeedback(childFeedback) } @@ -104,7 +108,9 @@ const GenerationItem: FC = ({ isLoading: isQuerying, feedback: childFeedback, onSave, - isMobile + isMobile, + isInstalledApp, + installedAppId, } const handleMoreLikeThis = async () => { @@ -113,7 +119,7 @@ const GenerationItem: FC = ({ return } startQuerying() - const res: any = await fetcMoreLikeThis(messageId as string) + const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId) setCompletionRes(res.answer) setChildMessageId(res.id) stopQuerying() diff --git a/web/app/components/explore/app-card/index.tsx b/web/app/components/explore/app-card/index.tsx new file mode 100644 index 0000000000..ff56b457f2 --- /dev/null +++ b/web/app/components/explore/app-card/index.tsx @@ -0,0 +1,65 @@ +'use client' +import cn from 'classnames' +import { useTranslation } from 'react-i18next' +import { App } from '@/models/explore' +import AppModeLabel from '@/app/(commonLayout)/apps/AppModeLabel' +import AppIcon from '@/app/components/base/app-icon' +import { PlusIcon } from '@heroicons/react/20/solid' +import Button from '../../base/button' + +import s from './style.module.css' + +const CustomizeBtn = ( + + + +) + +export type AppCardProps = { + app: App, + canCreate: boolean, + onCreate: () => void, + onAddToWorkspace: (appId: string) => void, +} + +const AppCard = ({ + app, + canCreate, + onCreate, + onAddToWorkspace, +}: AppCardProps) => { + const { t } = useTranslation() + const {app: appBasicInfo} = app + return ( +
+
+
+ +
+
{appBasicInfo.name}
+
+
+
{app.description}
+
+
+ +
+
+ + {canCreate && ( + + )} +
+
+
+
+ ) +} + +export default AppCard diff --git a/web/app/components/explore/app-card/style.module.css b/web/app/components/explore/app-card/style.module.css new file mode 100644 index 0000000000..dc0b96e448 --- /dev/null +++ b/web/app/components/explore/app-card/style.module.css @@ -0,0 +1,20 @@ +.wrap { + min-width: 312px; +} + +.mode { + display: flex; + height: 28px; +} + +.opWrap { + display: none; +} + +.wrap:hover .mode { + display: none; +} + +.wrap:hover .opWrap { + display: flex; +} \ No newline at end of file diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx new file mode 100644 index 0000000000..5470cebdec --- /dev/null +++ b/web/app/components/explore/app-list/index.tsx @@ -0,0 +1,130 @@ +'use client' +import React, { FC, useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import ExploreContext from '@/context/explore-context' +import { App } from '@/models/explore' +import Category from '@/app/components/explore/category' +import AppCard from '@/app/components/explore/app-card' +import { fetchAppList, installApp, fetchAppDetail } from '@/service/explore' +import { createApp } from '@/service/apps' +import CreateAppModal from '@/app/components/explore/create-app-modal' +import Loading from '@/app/components/base/loading' +import { NEED_REFRESH_APP_LIST_KEY } from '@/config' + +import s from './style.module.css' +import Toast from '../../base/toast' + +const Apps: FC = ({ }) => { + const { t } = useTranslation() + const router = useRouter() + const { setControlUpdateInstalledApps, hasEditPermission } = useContext(ExploreContext) + const [currCategory, setCurrCategory] = React.useState('') + const [allList, setAllList] = React.useState([]) + const [isLoaded, setIsLoaded] = React.useState(false) + + const currList = (() => { + if(currCategory === '') return allList + return allList.filter(item => item.category === currCategory) + })() + const [categories, setCategories] = React.useState([]) + useEffect(() => { + (async () => { + const {categories, recommended_apps}:any = await fetchAppList() + setCategories(categories) + setAllList(recommended_apps) + setIsLoaded(true) + })() + }, []) + + const handleAddToWorkspace = async (appId: string) => { + await installApp(appId) + Toast.notify({ + type: 'success', + message: t('common.api.success'), + }) + setControlUpdateInstalledApps(Date.now()) + } + + const [currApp, setCurrApp] = React.useState(null) + const [isShowCreateModal, setIsShowCreateModal] = React.useState(false) + const onCreate = async ({name, icon, icon_background}: any) => { + const { app_model_config: model_config } = await fetchAppDetail(currApp?.app.id as string) + + try { + const app = await createApp({ + name, + icon, + icon_background, + mode: currApp?.app.mode as any, + config: model_config, + }) + setIsShowCreateModal(false) + Toast.notify({ + type: 'success', + message: t('app.newApp.appCreated'), + }) + localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') + router.push(`/app/${app.id}/overview`) + } catch (e) { + Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) + } + } + + if(!isLoaded) { + return ( +
+ +
+ ) + } + + return ( +
+
+
{t('explore.apps.title')}
+
{t('explore.apps.description')}
+
+ +
+ +
+ + {isShowCreateModal && ( + setIsShowCreateModal(false)} + /> + )} +
+ ) +} + +export default React.memo(Apps) diff --git a/web/app/components/explore/app-list/style.module.css b/web/app/components/explore/app-list/style.module.css new file mode 100644 index 0000000000..856c884ec1 --- /dev/null +++ b/web/app/components/explore/app-list/style.module.css @@ -0,0 +1,17 @@ +@media (min-width: 1624px) { + .appList { + grid-template-columns: repeat(4, minmax(0, 1fr)) + } +} + +@media (min-width: 1300px) and (max-width: 1624px) { + .appList { + grid-template-columns: repeat(3, minmax(0, 1fr)) + } +} + +@media (min-width: 1025px) and (max-width: 1300px) { + .appList { + grid-template-columns: repeat(2, minmax(0, 1fr)) + } +} \ No newline at end of file diff --git a/web/app/components/explore/category.tsx b/web/app/components/explore/category.tsx new file mode 100644 index 0000000000..3c77b2aa2c --- /dev/null +++ b/web/app/components/explore/category.tsx @@ -0,0 +1,48 @@ +'use client' +import React, { FC } from 'react' +import { useTranslation } from 'react-i18next' +import exploreI18n from '@/i18n/lang/explore.en' +import cn from 'classnames' + +const categoryI18n = exploreI18n.category + +export interface ICategoryProps { + className?: string + list: string[] + value: string + onChange: (value: string) => void +} + +const Category: FC = ({ + className, + list, + value, + onChange +}) => { + const { t } = useTranslation() + + const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium','flex items-center h-7 px-3 border cursor-pointer rounded-lg') + const itemStyle = (isSelected: boolean) => isSelected ? {boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'} : {} + return ( +
+
onChange('')} + > + {t('explore.apps.allCategories')} +
+ {list.map(name => ( +
onChange(name)} + > + {(categoryI18n as any)[name] ? t(`explore.category.${name}`) : name} +
+ ))} +
+ ) +} +export default React.memo(Category) diff --git a/web/app/components/explore/create-app-modal/index.tsx b/web/app/components/explore/create-app-modal/index.tsx new file mode 100644 index 0000000000..5053d96bbf --- /dev/null +++ b/web/app/components/explore/create-app-modal/index.tsx @@ -0,0 +1,86 @@ +'use client' +import React, { useState } from 'react' +import cn from 'classnames' +import { useTranslation } from 'react-i18next' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Toast from '@/app/components/base/toast' +import AppIcon from '@/app/components/base/app-icon' +import EmojiPicker from '@/app/components/base/emoji-picker' + +import s from './style.module.css' + +type IProps = { + appName: string, + show: boolean, + onConfirm: (info: any) => void, + onHide: () => void, +} + +const CreateAppModal = ({ + appName, + show = false, + onConfirm, + onHide, +}: IProps) => { + const { t } = useTranslation() + + const [name, setName] = React.useState('') + + const [showEmojiPicker, setShowEmojiPicker] = useState(false) + const [emoji, setEmoji] = useState({ icon: '🍌', icon_background: '#FFEAD5' }) + + const submit = () => { + if(!name.trim()) { + Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') }) + return + } + onConfirm({ + name, + ...emoji, + }) + onHide() + } + + return ( + <> + + +
{t('explore.appCustomize.title', {name: appName})}
+
+
{t('explore.appCustomize.subTitle')}
+
+ { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} /> + setName(e.target.value)} + className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' + /> +
+
+
+ + +
+
+ {showEmojiPicker && { + console.log(icon, icon_background) + setEmoji({ icon, icon_background }) + setShowEmojiPicker(false) + }} + onClose={() => { + setEmoji({ icon: '🍌', icon_background: '#FFEAD5' }) + setShowEmojiPicker(false) + }} + />} + + + ) +} + +export default CreateAppModal diff --git a/web/app/components/explore/create-app-modal/style.module.css b/web/app/components/explore/create-app-modal/style.module.css new file mode 100644 index 0000000000..798a463b8c --- /dev/null +++ b/web/app/components/explore/create-app-modal/style.module.css @@ -0,0 +1,36 @@ +.modal { + position: relative; +} + +.modal .close { + position: absolute; + right: 16px; + top: 25px; + width: 32px; + height: 32px; + border-radius: 8px; + background: center no-repeat url(~@/app/components/datasets/create/assets/close.svg); + background-size: 16px; + cursor: pointer; +} + +.modal .title { + @apply mb-9; + font-weight: 600; + font-size: 20px; + line-height: 30px; + color: #101828; +} + +.modal .content { + @apply mb-9; + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: #101828; +} + +.subTitle { + margin-bottom: 8px; + font-weight: 500; +} \ No newline at end of file diff --git a/web/app/components/explore/index.tsx b/web/app/components/explore/index.tsx new file mode 100644 index 0000000000..54222293db --- /dev/null +++ b/web/app/components/explore/index.tsx @@ -0,0 +1,51 @@ +'use client' +import React, { FC, useEffect, useState } from 'react' +import ExploreContext from '@/context/explore-context' +import Sidebar from '@/app/components/explore/sidebar' +import { useAppContext } from '@/context/app-context' +import { fetchMembers } from '@/service/common' +import { InstalledApp } from '@/models/explore' + +export interface IExploreProps { + children: React.ReactNode +} + +const Explore: FC = ({ + children +}) => { + const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0) + const { userProfile } = useAppContext() + const [hasEditPermission, setHasEditPermission] = useState(false) + const [installedApps, setInstalledApps] = useState([]) + + useEffect(() => { + (async () => { + const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {}}) + if(!accounts) return + const currUser = accounts.find(account => account.id === userProfile.id) + setHasEditPermission(currUser?.role !== 'normal') + })() + }, []) + + return ( +
+ + +
+ {children} +
+
+
+ ) +} +export default React.memo(Explore) diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx new file mode 100644 index 0000000000..097a281def --- /dev/null +++ b/web/app/components/explore/installed-app/index.tsx @@ -0,0 +1,37 @@ +'use client' +import React, { FC } from 'react' +import { useContext } from 'use-context-selector' +import ExploreContext from '@/context/explore-context' +import ChatApp from '@/app/components/share/chat' +import TextGenerationApp from '@/app/components/share/text-generation' +import Loading from '@/app/components/base/loading' + +export interface IInstalledAppProps { + id: string +} + +const InstalledApp: FC = ({ + id, +}) => { + const { installedApps } = useContext(ExploreContext) + const installedApp = installedApps.find(item => item.id === id) + + if(!installedApp) { + return ( +
+ +
+ ) + } + + return ( +
+ {installedApp?.app.mode === 'chat' ? ( + + ): ( + + )} +
+ ) +} +export default React.memo(InstalledApp) diff --git a/web/app/components/explore/item-operation/index.tsx b/web/app/components/explore/item-operation/index.tsx new file mode 100644 index 0000000000..8675f9dce1 --- /dev/null +++ b/web/app/components/explore/item-operation/index.tsx @@ -0,0 +1,60 @@ +'use client' +import React, { FC } from 'react' +import cn from 'classnames' +import { useTranslation } from 'react-i18next' +import Popover from '@/app/components/base/popover' +import { TrashIcon } from '@heroicons/react/24/outline' + +import s from './style.module.css' + +const PinIcon = ( + + + +) + +export interface IItemOperationProps { + className?: string + isPinned: boolean + isShowDelete: boolean + togglePin: () => void + onDelete: () => void +} + +const ItemOperation: FC = ({ + className, + isPinned, + isShowDelete, + togglePin, + onDelete +}) => { + const { t } = useTranslation() + + return ( + { + e.stopPropagation() + }}> +
+ {PinIcon} + {isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')} +
+ {isShowDelete && ( +
+ + {t('explore.sidebar.action.delete')} +
+ )} + + + } + trigger='click' + position='br' + btnElement={
} + btnClassName={(open) => cn(className, s.btn, 'h-6 w-6 rounded-md border-none p-1', open && '!bg-gray-100 !shadow-none')} + className={`!w-[120px] h-fit !z-20`} + /> + ) +} +export default React.memo(ItemOperation) diff --git a/web/app/components/explore/item-operation/style.module.css b/web/app/components/explore/item-operation/style.module.css new file mode 100644 index 0000000000..b3618c9e1d --- /dev/null +++ b/web/app/components/explore/item-operation/style.module.css @@ -0,0 +1,31 @@ +.actionItem { + @apply h-9 py-2 px-3 mx-1 flex items-center gap-2 rounded-lg cursor-pointer; +} + + +.actionName { + @apply text-gray-700 text-sm; +} + +.commonIcon { + @apply w-4 h-4 inline-block align-middle; + background-repeat: no-repeat; + background-position: center center; + background-size: contain; +} + +.actionIcon { + @apply bg-gray-500; + mask-image: url(~@/app/components/datasets/documents/assets/action.svg); +} + +body .btn { + background: url(~@/app/components/datasets/documents/assets/action.svg) center center no-repeat transparent; + background-size: 16px 16px; + /* mask-image: ; */ +} + +body .btn:hover { + /* background-image: ; */ + background-color: #F2F4F7; +} \ No newline at end of file diff --git a/web/app/components/explore/sidebar/app-nav-item/index.tsx b/web/app/components/explore/sidebar/app-nav-item/index.tsx new file mode 100644 index 0000000000..9ed5213667 --- /dev/null +++ b/web/app/components/explore/sidebar/app-nav-item/index.tsx @@ -0,0 +1,73 @@ +'use client' +import cn from 'classnames' +import { useRouter } from 'next/navigation' +import ItemOperation from '@/app/components/explore/item-operation' +import AppIcon from '@/app/components/base/app-icon' + +import s from './style.module.css' + +export interface IAppNavItemProps { + name: string + id: string + icon: string + icon_background: string + isSelected: boolean + isPinned: boolean + togglePin: () => void + uninstallable: boolean + onDelete: (id: string) => void +} + +export default function AppNavItem({ + name, + id, + icon, + icon_background, + isSelected, + isPinned, + togglePin, + uninstallable, + onDelete, +}: IAppNavItemProps) { + const router = useRouter() + const url = `/explore/installed/${id}` + + return ( +
{ + router.push(url) // use Link causes popup item always trigger jump. Can not be solved by e.stopPropagation(). + }} + > +
+ {/*
*/} + +
{name}
+
+ { + !isSelected && ( +
e.stopPropagation()}> + onDelete(id)} + /> +
+ ) + } +
+ ) +} diff --git a/web/app/components/explore/sidebar/app-nav-item/style.module.css b/web/app/components/explore/sidebar/app-nav-item/style.module.css new file mode 100644 index 0000000000..8626f071bf --- /dev/null +++ b/web/app/components/explore/sidebar/app-nav-item/style.module.css @@ -0,0 +1,17 @@ +/* .item:hover, */ +.item.active { + border: 0.5px solid #EAECF0; + box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); + border-radius: 8px; + background: #FFFFFF; + color: #344054; + font-weight: 500; +} + +.opBtn { + visibility: hidden; +} + +.item:hover .opBtn { + visibility: visible; +} \ No newline at end of file diff --git a/web/app/components/explore/sidebar/index.tsx b/web/app/components/explore/sidebar/index.tsx new file mode 100644 index 0000000000..2d44676100 --- /dev/null +++ b/web/app/components/explore/sidebar/index.tsx @@ -0,0 +1,128 @@ +'use client' +import React, { FC, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useContext } from 'use-context-selector' +import ExploreContext from '@/context/explore-context' +import cn from 'classnames' +import { useSelectedLayoutSegments } from 'next/navigation' +import Link from 'next/link' +import Item from './app-nav-item' +import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore' +import Toast from '../../base/toast' +import Confirm from '@/app/components/base/confirm' + +const SelectedDiscoveryIcon = () => ( + + + +) + +const DiscoveryIcon = () => ( + + + +) + +const SideBar: FC<{ + controlUpdateInstalledApps: number, +}> = ({ + controlUpdateInstalledApps, +}) => { + const { t } = useTranslation() + const segments = useSelectedLayoutSegments() + const lastSegment = segments.slice(-1)[0] + const isDiscoverySelected = lastSegment === 'apps' + const { installedApps, setInstalledApps } = useContext(ExploreContext) + + const fetchInstalledAppList = async () => { + const {installed_apps} : any = await doFetchInstalledAppList() + setInstalledApps(installed_apps) + } + + const [showConfirm, setShowConfirm] = useState(false) + const [currId, setCurrId] = useState('') + const handleDelete = async () => { + const id = currId + await uninstallApp(id) + setShowConfirm(false) + Toast.notify({ + type: 'success', + message: t('common.api.remove') + }) + fetchInstalledAppList() + } + + const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { + await updatePinStatus(id, isPinned) + Toast.notify({ + type: 'success', + message: t('common.api.success') + }) + fetchInstalledAppList() + } + + useEffect(() => { + fetchInstalledAppList() + }, []) + + useEffect(() => { + fetchInstalledAppList() + }, [controlUpdateInstalledApps]) + + return ( +
+
+ + {isDiscoverySelected ? : } +
{t('explore.sidebar.discovery')}
+ +
+ {installedApps.length > 0 && ( +
+
{t('explore.sidebar.workspace')}
+
+ {installedApps.map(({id, is_pinned, uninstallable, app : { name, icon, icon_background }}) => { + return ( + handleUpdatePinStatus(id, !is_pinned)} + uninstallable={uninstallable} + onDelete={(id) => { + setCurrId(id) + setShowConfirm(true) + }} + /> + ) + })} +
+
+ )} + {showConfirm && ( + setShowConfirm(false)} + onConfirm={handleDelete} + onCancel={() => setShowConfirm(false)} + /> + )} +
+ ) +} + +export default React.memo(SideBar) diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index d968f7b49b..182a466559 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -15,6 +15,12 @@ import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog' import { WorkspaceProvider } from '@/context/workspace-context' import { useDatasetsContext } from '@/context/datasets-context' +const BuildAppsIcon = ({isSelected}: {isSelected: boolean}) => ( + + + +) + export type IHeaderProps = { appItems: AppDetailResponse[] curApp: AppDetailResponse @@ -38,8 +44,9 @@ const Header: FC = ({ appItems, curApp, userProfile, onLogout, lan const { datasets, currentDataset } = useDatasetsContext() const router = useRouter() const showEnvTag = langeniusVersionInfo.current_env === 'TESTING' || langeniusVersionInfo.current_env === 'DEVELOPMENT' - const isPluginsComingSoon = useSelectedLayoutSegment() === 'plugins-coming-soon' - + const selectedSegment = useSelectedLayoutSegment() + const isPluginsComingSoon = selectedSegment === 'plugins-coming-soon' + const isExplore = selectedSegment === 'explore' return (
= ({ appItems, curApp, userProfile, onLogout, lan
+ {/* + + {t('common.menus.explore')} + */}