You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

app-sidebar-dropdown.tsx 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import React, { useCallback, useRef, useState } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import { useAppContext } from '@/context/app-context'
  4. import {
  5. RiEqualizer2Line,
  6. RiMenuLine,
  7. } from '@remixicon/react'
  8. import {
  9. PortalToFollowElem,
  10. PortalToFollowElemContent,
  11. PortalToFollowElemTrigger,
  12. } from '@/app/components/base/portal-to-follow-elem'
  13. import AppIcon from '../base/app-icon'
  14. import Divider from '../base/divider'
  15. import AppInfo from './app-info'
  16. import NavLink from './navLink'
  17. import { useStore as useAppStore } from '@/app/components/app/store'
  18. import type { NavIcon } from './navLink'
  19. import cn from '@/utils/classnames'
  20. type Props = {
  21. navigation: Array<{
  22. name: string
  23. href: string
  24. icon: NavIcon
  25. selectedIcon: NavIcon
  26. }>
  27. }
  28. const AppSidebarDropdown = ({ navigation }: Props) => {
  29. const { t } = useTranslation()
  30. const { isCurrentWorkspaceEditor } = useAppContext()
  31. const appDetail = useAppStore(state => state.appDetail)
  32. const [detailExpand, setDetailExpand] = useState(false)
  33. const [open, doSetOpen] = useState(false)
  34. const openRef = useRef(open)
  35. const setOpen = useCallback((v: boolean) => {
  36. doSetOpen(v)
  37. openRef.current = v
  38. }, [doSetOpen])
  39. const handleTrigger = useCallback(() => {
  40. setOpen(!openRef.current)
  41. }, [setOpen])
  42. if (!appDetail)
  43. return null
  44. return (
  45. <>
  46. <div className='fixed left-2 top-2 z-20'>
  47. <PortalToFollowElem
  48. open={open}
  49. onOpenChange={setOpen}
  50. placement='bottom-start'
  51. offset={{
  52. mainAxis: -41,
  53. }}
  54. >
  55. <PortalToFollowElemTrigger onClick={handleTrigger}>
  56. <div className={cn('flex cursor-pointer items-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-1 shadow-lg backdrop-blur-sm hover:bg-background-default-hover', open && 'bg-background-default-hover')}>
  57. <AppIcon
  58. size='small'
  59. iconType={appDetail.icon_type}
  60. icon={appDetail.icon}
  61. background={appDetail.icon_background}
  62. imageUrl={appDetail.icon_url}
  63. />
  64. <RiMenuLine className='h-4 w-4 text-text-tertiary' />
  65. </div>
  66. </PortalToFollowElemTrigger>
  67. <PortalToFollowElemContent className='z-[1000]'>
  68. <div className={cn('w-[305px] rounded-xl border-[0.5px] border-components-panel-border bg-background-default-subtle shadow-lg')}>
  69. <div className='p-2'>
  70. <div
  71. className={cn('flex flex-col gap-2 rounded-lg p-2 pb-2.5', isCurrentWorkspaceEditor && 'cursor-pointer hover:bg-state-base-hover')}
  72. onClick={() => {
  73. setDetailExpand(true)
  74. setOpen(false)
  75. }}
  76. >
  77. <div className='flex items-center justify-between self-stretch'>
  78. <AppIcon
  79. size='large'
  80. iconType={appDetail.icon_type}
  81. icon={appDetail.icon}
  82. background={appDetail.icon_background}
  83. imageUrl={appDetail.icon_url}
  84. />
  85. <div className='flex items-center justify-center rounded-md p-0.5'>
  86. <div className='flex h-5 w-5 items-center justify-center'>
  87. <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
  88. </div>
  89. </div>
  90. </div>
  91. <div className='flex flex-col items-start gap-1'>
  92. <div className='flex w-full'>
  93. <div className='system-md-semibold truncate text-text-secondary'>{appDetail.name}</div>
  94. </div>
  95. <div className='system-2xs-medium-uppercase text-text-tertiary'>{appDetail.mode === 'advanced-chat' ? t('app.types.advanced') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
  96. </div>
  97. </div>
  98. </div>
  99. <div className='px-4'>
  100. <Divider bgStyle='gradient' />
  101. </div>
  102. <nav className='space-y-0.5 px-3 pb-6 pt-4'>
  103. {navigation.map((item, index) => {
  104. return (
  105. <NavLink key={index} mode='expand' iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
  106. )
  107. })}
  108. </nav>
  109. </div>
  110. </PortalToFollowElemContent>
  111. </PortalToFollowElem>
  112. </div>
  113. <div className='z-20'>
  114. <AppInfo expand onlyShowDetail openState={detailExpand} onDetailExpand={setDetailExpand} />
  115. </div>
  116. </>
  117. )
  118. }
  119. export default AppSidebarDropdown