Initial commit

This commit is contained in:
2025-10-14 14:17:21 +08:00
commit ac715a8b88
35011 changed files with 3834178 additions and 0 deletions

View File

@@ -0,0 +1,474 @@
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
import { RiArrowDownSLine } from '@remixicon/react'
import React, { useCallback, useState } from 'react'
import AppIcon from '../base/app-icon'
import SwitchAppModal from '../app/switch-app-modal'
import s from './style.module.css'
import cn from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Divider from '@/app/components/base/divider'
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 { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
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'
export type IAppInfoProps = {
expand: boolean
}
const AppInfo = ({ expand }: IAppInfoProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { replace } = useRouter()
const { onPlanInfoChanged } = useProviderContext()
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
const [open, setOpen] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showSwitchTip, setShowSwitchTip] = useState<string>('')
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const mutateApps = useContextSelector(
AppsContext,
state => state.mutateApps,
)
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
icon_type,
icon,
icon_background,
description,
use_icon_as_answer_icon,
}) => {
if (!appDetail)
return
try {
const app = await updateAppInfo({
appID: appDetail.id,
name,
icon_type,
icon,
icon_background,
description,
use_icon_as_answer_icon,
})
setShowEditModal(false)
notify({
type: 'success',
message: t('app.editDone'),
})
setAppDetail(app)
mutateApps()
}
catch (e) {
notify({ type: 'error', message: t('app.editFailed') })
}
}, [appDetail, mutateApps, notify, setAppDetail, t])
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => {
if (!appDetail)
return
try {
const newApp = await copyApp({
appID: appDetail.id,
name,
icon_type,
icon,
icon_background,
mode: appDetail.mode,
})
setShowDuplicateModal(false)
notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
mutateApps()
onPlanInfoChanged()
getRedirection(true, newApp, replace)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
const onExport = async (include = false) => {
if (!appDetail)
return
try {
const { data } = await exportAppConfig({
appID: appDetail.id,
include,
})
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
a.href = URL.createObjectURL(file)
a.download = `${appDetail.name}.yml`
a.click()
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
}
const exportCheck = async () => {
if (!appDetail)
return
if (appDetail.mode !== 'workflow' && appDetail.mode !== 'advanced-chat') {
onExport()
return
}
try {
const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`)
const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
if (list.length === 0) {
onExport()
return
}
setSecretEnvList(list)
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
}
const onConfirmDelete = useCallback(async () => {
if (!appDetail)
return
try {
await deleteApp(appDetail.id)
notify({ type: 'success', message: t('app.appDeleted') })
mutateApps()
onPlanInfoChanged()
setAppDetail()
replace('/apps')
}
catch (e: any) {
notify({
type: 'error',
message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
})
}
setShowConfirmDelete(false)
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t])
const { isCurrentWorkspaceEditor } = useAppContext()
if (!appDetail)
return null
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger
onClick={() => {
if (isCurrentWorkspaceEditor)
setOpen(v => !v)
}}
className='block'
>
<div className={cn('flex p-1 rounded-lg', open && 'bg-gray-100', isCurrentWorkspaceEditor && 'hover:bg-gray-100 cursor-pointer')}>
<div className='relative shrink-0 mr-2'>
<AppIcon
size={expand ? 'large' : 'small'}
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<span className={cn(
'absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm',
!expand && '!w-3.5 !h-3.5 !bottom-[-2px] !right-[-2px]',
)}>
{appDetail.mode === 'advanced-chat' && (
<ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
)}
{appDetail.mode === 'agent-chat' && (
<CuteRobot className={cn('w-3 h-3 text-indigo-600', !expand && '!w-2.5 !h-2.5')} />
)}
{appDetail.mode === 'chat' && (
<ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
)}
{appDetail.mode === 'completion' && (
<AiText className={cn('w-3 h-3 text-[#0E9384]', !expand && '!w-2.5 !h-2.5')} />
)}
{appDetail.mode === 'workflow' && (
<Route className={cn('w-3 h-3 text-[#f79009]', !expand && '!w-2.5 !h-2.5')} />
)}
</span>
</div>
{expand && (
<div className="grow w-0">
<div className='flex justify-between items-center text-sm leading-5 font-medium text-text-secondary'>
<div className='truncate' title={appDetail.name}>{appDetail.name}</div>
{isCurrentWorkspaceEditor && <RiArrowDownSLine className='shrink-0 ml-[2px] w-3 h-3 text-gray-500' />}
</div>
<div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
{appDetail.mode === 'advanced-chat' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
<div title={t('app.types.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.advanced').toUpperCase()}</div>
</>
)}
{appDetail.mode === 'agent-chat' && (
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
)}
{appDetail.mode === 'chat' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
</>
)}
{appDetail.mode === 'completion' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
</>
)}
{appDetail.mode === 'workflow' && (
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
)}
</div>
</div>
)}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='relative w-[320px] bg-white rounded-2xl shadow-xl'>
{/* header */}
<div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-2')}>
<div className='relative shrink-0 mr-2'>
<AppIcon
size="large"
iconType={appDetail.icon_type}
icon={appDetail.icon}
background={appDetail.icon_background}
imageUrl={appDetail.icon_url}
/>
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
{appDetail.mode === 'advanced-chat' && (
<ChatBot className='w-3 h-3 text-[#1570EF]' />
)}
{appDetail.mode === 'agent-chat' && (
<CuteRobot className='w-3 h-3 text-indigo-600' />
)}
{appDetail.mode === 'chat' && (
<ChatBot className='w-3 h-3 text-[#1570EF]' />
)}
{appDetail.mode === 'completion' && (
<AiText className='w-3 h-3 text-[#0E9384]' />
)}
{appDetail.mode === 'workflow' && (
<Route className='w-3 h-3 text-[#f79009]' />
)}
</span>
</div>
<div className='grow w-0'>
<div title={appDetail.name} className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900 truncate'>{appDetail.name}</div>
<div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
{appDetail.mode === 'advanced-chat' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
<div title={t('app.types.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.advanced').toUpperCase()}</div>
</>
)}
{appDetail.mode === 'agent-chat' && (
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
)}
{appDetail.mode === 'chat' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
</>
)}
{appDetail.mode === 'completion' && (
<>
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
</>
)}
{appDetail.mode === 'workflow' && (
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
)}
</div>
</div>
</div>
{/* description */}
{appDetail.description && (
<div className='px-4 py-2 text-gray-500 text-xs leading-[18px]'>{appDetail.description}</div>
)}
{/* operations */}
<Divider className="!my-1" />
<div className="w-full py-1">
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowEditModal(true)
}}>
<span className='text-gray-700 text-sm leading-5'>{t('app.editApp')}</span>
</div>
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowDuplicateModal(true)
}}>
<span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span>
</div>
{(appDetail.mode === 'completion' || appDetail.mode === 'chat') && (
<>
<Divider className="!my-1" />
<div
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
onMouseEnter={() => setShowSwitchTip(appDetail.mode)}
onMouseLeave={() => setShowSwitchTip('')}
onClick={() => {
setOpen(false)
setShowSwitchModal(true)
}}
>
<span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
</div>
</>
)}
<Divider className="!my-1" />
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={exportCheck}>
<span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
</div>
{
(appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (
<div
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
onClick={() => {
setOpen(false)
setShowImportDSLModal(true)
}}>
<span className='text-gray-700 text-sm leading-5'>{t('workflow.common.importDSL')}</span>
</div>
)
}
<Divider className="!my-1" />
<div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowConfirmDelete(true)
}}>
<span className='text-gray-700 text-sm leading-5 group-hover:text-red-500'>
{t('common.operation.delete')}
</span>
</div>
</div>
{/* switch tip */}
<div
className={cn(
'hidden absolute left-[324px] top-0 w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg',
showSwitchTip && '!block',
)}
>
<div className={cn(
'w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl',
showSwitchTip === 'chat' && s.expertPic,
showSwitchTip === 'completion' && s.completionPic,
)} />
<div className='px-4 pb-2'>
<div className='flex items-center gap-1 text-gray-700 text-md leading-6 font-semibold'>
{showSwitchTip === 'chat' ? t('app.types.advanced') : t('app.types.workflow')}
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
</div>
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.advancedFor').toLocaleUpperCase()}</div>
<div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.advancedDescription')}</div>
</div>
</div>
</div>
</PortalToFollowElemContent>
{showSwitchModal && (
<SwitchAppModal
inAppDetail
show={showSwitchModal}
appDetail={appDetail}
onClose={() => setShowSwitchModal(false)}
onSuccess={() => setShowSwitchModal(false)}
/>
)}
{showEditModal && (
<CreateAppModal
isEditModal
appName={appDetail.name}
appIconType={appDetail.icon_type}
appIcon={appDetail.icon}
appIconBackground={appDetail.icon_background}
appIconUrl={appDetail.icon_url}
appDescription={appDetail.description}
appMode={appDetail.mode}
appUseIconAsAnswerIcon={appDetail.use_icon_as_answer_icon}
show={showEditModal}
onConfirm={onEdit}
onHide={() => setShowEditModal(false)}
/>
)}
{showDuplicateModal && (
<DuplicateAppModal
appName={appDetail.name}
icon_type={appDetail.icon_type}
icon={appDetail.icon}
icon_background={appDetail.icon_background}
icon_url={appDetail.icon_url}
show={showDuplicateModal}
onConfirm={onCopy}
onHide={() => setShowDuplicateModal(false)}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('app.deleteAppConfirmTitle')}
content={t('app.deleteAppConfirmContent')}
isShow={showConfirmDelete}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
{showImportDSLModal && (
<UpdateDSLModal
onCancel={() => setShowImportDSLModal(false)}
onBackup={exportCheck}
/>
)}
{secretEnvList.length > 0 && (
<DSLExportConfirmModal
envList={secretEnvList}
onConfirm={onExport}
onClose={() => setSecretEnvList([])}
/>
)}
</div>
</PortalToFollowElem>
)
}
export default React.memo(AppInfo)

View File

@@ -0,0 +1,94 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '../base/app-icon'
import Tooltip from '@/app/components/base/tooltip'
export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
icon?: string
icon_background?: string | null
isExternal?: boolean
name: string
type: string | React.ReactNode
hoverTip?: string
textStyle?: { main?: string; extra?: string }
isExtraInLine?: boolean
mode?: string
}
const ApiSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M12.5 9H1.5" stroke="#5850EC" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
</svg>
const WebappSvg = <svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.375 5.45825L7.99998 8.99992M7.99998 8.99992L1.62498 5.45825M7.99998 8.99992L8 16.1249M14.75 12.0439V5.95603C14.75 5.69904 14.75 5.57055 14.7121 5.45595C14.6786 5.35457 14.6239 5.26151 14.5515 5.18299C14.4697 5.09424 14.3574 5.03184 14.1328 4.90704L8.58277 1.8237C8.37007 1.70553 8.26372 1.64645 8.15109 1.62329C8.05141 1.60278 7.9486 1.60278 7.84891 1.62329C7.73628 1.64645 7.62993 1.70553 7.41723 1.8237L1.86723 4.90704C1.64259 5.03184 1.53026 5.09424 1.44847 5.18299C1.37612 5.26151 1.32136 5.35457 1.28786 5.45595C1.25 5.57055 1.25 5.69904 1.25 5.95603V12.0439C1.25 12.3008 1.25 12.4293 1.28786 12.5439C1.32136 12.6453 1.37612 12.7384 1.44847 12.8169C1.53026 12.9056 1.64259 12.968 1.86723 13.0928L7.41723 16.1762C7.62993 16.2943 7.73628 16.3534 7.84891 16.3766C7.9486 16.3971 8.05141 16.3971 8.15109 16.3766C8.26372 16.3534 8.37007 16.2943 8.58277 16.1762L14.1328 13.0928C14.3574 12.968 14.4697 12.9056 14.5515 12.8169C14.6239 12.7384 14.6786 12.6453 14.7121 12.5439C14.75 12.4293 14.75 12.3008 14.75 12.0439Z" stroke="#155EEF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
const NotionSvg = <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_6294_13848)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.287 21.9133L1.70748 18.6999C1.08685 17.9267 0.75 16.976 0.75 15.9974V4.36124C0.75 2.89548 1.92269 1.67923 3.43553 1.57594L15.3991 0.759137C16.2682 0.699797 17.1321 0.930818 17.8461 1.41353L22.0494 4.25543C22.8018 4.76414 23.25 5.59574 23.25 6.48319V19.7124C23.25 21.1468 22.0969 22.3345 20.6157 22.4256L7.3375 23.243C6.1555 23.3158 5.01299 22.8178 4.287 21.9133Z" fill="white" />
<path d="M8.43607 10.1842V10.0318C8.43607 9.64564 8.74535 9.32537 9.14397 9.29876L12.0475 9.10491L16.0628 15.0178V9.82823L15.0293 9.69046V9.6181C15.0293 9.22739 15.3456 8.90501 15.7493 8.88433L18.3912 8.74899V9.12918C18.3912 9.30765 18.2585 9.46031 18.0766 9.49108L17.4408 9.59861V18.0029L16.6429 18.2773C15.9764 18.5065 15.2343 18.2611 14.8527 17.6853L10.9545 11.803V17.4173L12.1544 17.647L12.1377 17.7583C12.0853 18.1069 11.7843 18.3705 11.4202 18.3867L8.43607 18.5195C8.39662 18.1447 8.67758 17.8093 9.06518 17.7686L9.45771 17.7273V10.2416L8.43607 10.1842Z" fill="black" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5062 2.22521L3.5426 3.04201C2.82599 3.09094 2.27051 3.66706 2.27051 4.36136V15.9975C2.27051 16.6499 2.49507 17.2837 2.90883 17.7992L5.48835 21.0126C5.90541 21.5322 6.56174 21.8183 7.24076 21.7765L20.519 20.9591C21.1995 20.9172 21.7293 20.3716 21.7293 19.7125V6.48332C21.7293 6.07557 21.5234 5.69348 21.1777 5.45975L16.9743 2.61784C16.546 2.32822 16.0277 2.1896 15.5062 2.22521ZM4.13585 4.54287C3.96946 4.41968 4.04865 4.16303 4.25768 4.14804L15.5866 3.33545C15.9476 3.30956 16.3063 3.40896 16.5982 3.61578L18.8713 5.22622C18.9576 5.28736 18.9171 5.41935 18.8102 5.42516L6.8129 6.07764C6.44983 6.09739 6.09144 5.99073 5.80276 5.77699L4.13585 4.54287ZM6.25018 8.12315C6.25018 7.7334 6.56506 7.41145 6.9677 7.38952L19.6523 6.69871C20.0447 6.67734 20.375 6.97912 20.375 7.35898V18.8141C20.375 19.2031 20.0613 19.5247 19.6594 19.5476L7.05516 20.2648C6.61845 20.2896 6.25018 19.954 6.25018 19.5312V8.12315Z" fill="black" />
</g>
<defs>
<clipPath id="clip0_6294_13848">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
const ICON_MAP = {
app: <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
api: <AppIcon innerIcon={ApiSvg} className='border !bg-purple-50 !border-purple-200' />,
dataset: <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />,
webapp: <AppIcon innerIcon={WebappSvg} className='border !bg-primary-100 !border-primary-200' />,
notion: <AppIcon innerIcon={NotionSvg} className='!border-[0.5px] !border-indigo-100 !bg-white' />,
}
export default function AppBasic({ icon, icon_background, name, isExternal, type, hoverTip, textStyle, mode = 'expand', iconType = 'app' }: IAppBasicProps) {
const { t } = useTranslation()
return (
<div className="flex items-start p-1">
{icon && icon_background && iconType === 'app' && (
<div className='flex-shrink-0 mr-3'>
<AppIcon icon={icon} background={icon_background} />
</div>
)}
{iconType !== 'app'
&& <div className='flex-shrink-0 mr-3'>
{ICON_MAP[iconType]}
</div>
}
{mode === 'expand' && <div className="group">
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 break-all ${textStyle?.main ?? ''}`}>
{name}
{hoverTip
&& <Tooltip
popupContent={
<div className='w-[240px]'>
{hoverTip}
</div>
}
popupClassName='ml-1'
triggerClassName='w-4 h-4 ml-1'
position='top'
/>
}
</div>
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 break-all ${textStyle?.extra ?? ''}`}>{type}</div>
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : ''}</div>
</div>}
</div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@@ -0,0 +1,45 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '../base/app-icon'
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
</svg>
type Props = {
isExternal?: boolean
name: string
description: string
expand: boolean
extraInfo?: React.ReactNode
}
const DatasetInfo: FC<Props> = ({
name,
description,
isExternal,
expand,
extraInfo,
}) => {
const { t } = useTranslation()
return (
<div className='pl-1 pt-1'>
<div className='flex-shrink-0 mr-3'>
<AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />
</div>
{expand && (
<div className='mt-2'>
<div className='system-md-semibold text-text-secondary'>
{name}
</div>
<div className='mt-1 text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : t('dataset.localDocs')}</div>
<div className='my-3 system-xs-regular text-text-tertiary first-letter:capitalize'>{description}</div>
</div>
)}
{extraInfo}
</div>
)
}
export default React.memo(DatasetInfo)

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1,127 @@
import React, { useEffect } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { RiLayoutRight2Line } from '@remixicon/react'
import { LayoutRight2LineMod } from '../base/icons/src/public/knowledge'
import NavLink from './navLink'
import type { NavIcon } from './navLink'
import AppBasic from './basic'
import AppInfo from './app-info'
import DatasetInfo from './dataset-info'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useStore as useAppStore } from '@/app/components/app/store'
import cn from '@/utils/classnames'
export type IAppDetailNavProps = {
iconType?: 'app' | 'dataset' | 'notion'
title: string
desc: string
isExternal?: boolean
icon: string
icon_background: string
navigation: Array<{
name: string
href: string
icon: NavIcon
selectedIcon: NavIcon
}>
extraInfo?: (modeState: string) => React.ReactNode
}
const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigation, extraInfo, iconType = 'app' }: IAppDetailNavProps) => {
const { appSidebarExpand, setAppSiderbarExpand } = useAppStore(useShallow(state => ({
appSidebarExpand: state.appSidebarExpand,
setAppSiderbarExpand: state.setAppSiderbarExpand,
})))
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const expand = appSidebarExpand === 'expand'
const handleToggle = (state: string) => {
setAppSiderbarExpand(state === 'expand' ? 'collapse' : 'expand')
}
useEffect(() => {
if (appSidebarExpand) {
localStorage.setItem('app-detail-collapse-or-expand', appSidebarExpand)
setAppSiderbarExpand(appSidebarExpand)
}
}, [appSidebarExpand, setAppSiderbarExpand])
return (
<div
className={`
shrink-0 flex flex-col bg-background-default-subtle border-r border-divider-burn transition-all
${expand ? 'w-[216px]' : 'w-14'}
`}
>
<div
className={`
shrink-0
${expand ? 'p-3' : 'p-2'}
`}
>
{iconType === 'app' && (
<AppInfo expand={expand} />
)}
{iconType === 'dataset' && (
<DatasetInfo
name={title}
description={desc}
isExternal={isExternal}
expand={expand}
extraInfo={extraInfo && extraInfo(appSidebarExpand)}
/>
)}
{!['app', 'dataset'].includes(iconType) && (
<AppBasic
mode={appSidebarExpand}
iconType={iconType}
icon={icon}
icon_background={icon_background}
name={title}
type={desc}
isExternal={isExternal}
/>
)}
</div>
<div className='px-4'>
<div className={cn('mt-1 mx-auto h-[1px] bg-divider-subtle', !expand && 'w-6')} />
</div>
<nav
className={`
grow space-y-1
${expand ? 'p-4' : 'px-2.5 py-4'}
`}
>
{navigation.map((item, index) => {
return (
<NavLink key={index} mode={appSidebarExpand} iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
)
})}
</nav>
{
!isMobile && (
<div
className={`
shrink-0 py-3
${expand ? 'px-6' : 'px-4'}
`}
>
<div
className='flex items-center justify-center w-6 h-6 text-gray-500 cursor-pointer'
onClick={() => handleToggle(appSidebarExpand)}
>
{
expand
? <RiLayoutRight2Line className='w-5 h-5 text-components-menu-item-text' />
: <LayoutRight2LineMod className='w-5 h-5 text-components-menu-item-text' />
}
</div>
</div>
)
}
</div>
)
}
export default React.memo(AppDetailNav)

View File

@@ -0,0 +1,63 @@
'use client'
import { useSelectedLayoutSegment } from 'next/navigation'
import Link from 'next/link'
import classNames from '@/utils/classnames'
export type NavIcon = React.ComponentType<
React.PropsWithoutRef<React.ComponentProps<'svg'>> & {
title?: string | undefined
titleId?: string | undefined
}
>
export type NavLinkProps = {
name: string
href: string
iconMap: {
selected: NavIcon
normal: NavIcon
}
mode?: string
}
export default function NavLink({
name,
href,
iconMap,
mode = 'expand',
}: NavLinkProps) {
const segment = useSelectedLayoutSegment()
const formattedSegment = (() => {
let res = segment?.toLowerCase()
// logs and annotations use the same nav
if (res === 'annotations')
res = 'logs'
return res
})()
const isActive = href.toLowerCase().split('/')?.pop() === formattedSegment
const NavIcon = isActive ? iconMap.selected : iconMap.normal
return (
<Link
key={name}
href={href}
className={classNames(
isActive ? 'bg-state-accent-active text-text-accent font-semibold' : 'text-components-menu-item-text hover:bg-gray-100 hover:text-components-menu-item-text-hover',
'group flex items-center h-9 rounded-md py-2 text-sm font-normal',
mode === 'expand' ? 'px-3' : 'px-2.5',
)}
title={mode === 'collapse' ? name : ''}
>
<NavIcon
className={classNames(
'h-4 w-4 flex-shrink-0',
mode === 'expand' ? 'mr-2' : 'mr-0',
)}
aria-hidden="true"
/>
{mode === 'expand' && name}
</Link>
)
}

View File

@@ -0,0 +1,11 @@
.sidebar {
border-right: 1px solid #F3F4F6;
}
.completionPic {
background-image: url('./completion.png')
}
.expertPic {
background-image: url('./expert.png')
}