Bladeren bron

feat: partner badge in marketplace (#14258)

Wu Tianwei 3 maanden geleden
bovenliggende
commit
42b13bd312
31 gewijzigde bestanden met toevoegingen van 2288 en 31 verwijderingen
  1. 2 1
      web/app/components/app/create-app-modal/index.tsx
  2. 58 0
      web/app/components/base/icons/assets/public/plugins/partner-dark.svg
  3. 58 0
      web/app/components/base/icons/assets/public/plugins/partner-light.svg
  4. 58 0
      web/app/components/base/icons/assets/public/plugins/verified-dark.svg
  5. 58 0
      web/app/components/base/icons/assets/public/plugins/verified-light.svg
  6. 447 0
      web/app/components/base/icons/src/public/plugins/PartnerDark.json
  7. 16 0
      web/app/components/base/icons/src/public/plugins/PartnerDark.tsx
  8. 446 0
      web/app/components/base/icons/src/public/plugins/PartnerLight.json
  9. 16 0
      web/app/components/base/icons/src/public/plugins/PartnerLight.tsx
  10. 457 0
      web/app/components/base/icons/src/public/plugins/VerifiedDark.json
  11. 16 0
      web/app/components/base/icons/src/public/plugins/VerifiedDark.tsx
  12. 456 0
      web/app/components/base/icons/src/public/plugins/VerifiedLight.json
  13. 16 0
      web/app/components/base/icons/src/public/plugins/VerifiedLight.tsx
  14. 4 0
      web/app/components/base/icons/src/public/plugins/index.ts
  15. 2 2
      web/app/components/datasets/create/file-uploader/index.tsx
  16. 2 2
      web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx
  17. 37 0
      web/app/components/plugins/base/badges/icon-with-tooltip.tsx
  18. 29 0
      web/app/components/plugins/base/badges/partner.tsx
  19. 29 0
      web/app/components/plugins/base/badges/verified.tsx
  20. 6 3
      web/app/components/plugins/card/index.tsx
  21. 2 1
      web/app/components/plugins/install-plugin/utils.ts
  22. 2 0
      web/app/components/plugins/types.ts
  23. 2 2
      web/app/components/workflow/run/status-container.tsx
  24. 12 2
      web/app/layout.tsx
  25. 0 18
      web/context/app-context.tsx
  26. 13 0
      web/hooks/use-theme.ts
  27. 2 0
      web/i18n/en-US/plugin.ts
  28. 2 0
      web/i18n/zh-Hans/plugin.ts
  29. 7 0
      web/package.json
  30. 32 0
      web/pnpm-lock.yaml
  31. 1 0
      web/types/app.ts

+ 2 - 1
web/app/components/app/create-app-modal/index.tsx

@@ -27,6 +27,7 @@ import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/bas
 import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
 import { getRedirection } from '@/utils/app-redirection'
 import FullScreenModal from '@/app/components/base/fullscreen-modal'
+import useTheme from '@/hooks/use-theme'
 
 type CreateAppProps = {
   onSuccess: () => void
@@ -346,7 +347,7 @@ function AppPreview({ mode }: { mode: AppMode }) {
 }
 
 function AppScreenShot({ mode, show }: { mode: AppMode; show: boolean }) {
-  const theme = useContextSelector(AppsContext, state => state.theme)
+  const { theme } = useTheme()
   const modeToImageMap = {
     'chat': 'Chatbot',
     'advanced-chat': 'Chatflow',

File diff suppressed because it is too large
+ 58 - 0
web/app/components/base/icons/assets/public/plugins/partner-dark.svg


File diff suppressed because it is too large
+ 58 - 0
web/app/components/base/icons/assets/public/plugins/partner-light.svg


File diff suppressed because it is too large
+ 58 - 0
web/app/components/base/icons/assets/public/plugins/verified-dark.svg


File diff suppressed because it is too large
+ 58 - 0
web/app/components/base/icons/assets/public/plugins/verified-light.svg


File diff suppressed because it is too large
+ 447 - 0
web/app/components/base/icons/src/public/plugins/PartnerDark.json


+ 16 - 0
web/app/components/base/icons/src/public/plugins/PartnerDark.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './PartnerDark.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'PartnerDark'
+
+export default Icon

File diff suppressed because it is too large
+ 446 - 0
web/app/components/base/icons/src/public/plugins/PartnerLight.json


+ 16 - 0
web/app/components/base/icons/src/public/plugins/PartnerLight.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './PartnerLight.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'PartnerLight'
+
+export default Icon

File diff suppressed because it is too large
+ 457 - 0
web/app/components/base/icons/src/public/plugins/VerifiedDark.json


+ 16 - 0
web/app/components/base/icons/src/public/plugins/VerifiedDark.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './VerifiedDark.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'VerifiedDark'
+
+export default Icon

File diff suppressed because it is too large
+ 456 - 0
web/app/components/base/icons/src/public/plugins/VerifiedLight.json


+ 16 - 0
web/app/components/base/icons/src/public/plugins/VerifiedLight.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './VerifiedLight.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'VerifiedLight'
+
+export default Icon

+ 4 - 0
web/app/components/base/icons/src/public/plugins/index.ts

@@ -1,3 +1,7 @@
 export { default as Google } from './Google'
+export { default as PartnerDark } from './PartnerDark'
+export { default as PartnerLight } from './PartnerLight'
+export { default as VerifiedDark } from './VerifiedDark'
+export { default as VerifiedLight } from './VerifiedLight'
 export { default as WebReader } from './WebReader'
 export { default as Wikipedia } from './Wikipedia'

+ 2 - 2
web/app/components/datasets/create/file-uploader/index.tsx

@@ -16,8 +16,8 @@ import { fetchSupportFileTypes } from '@/service/datasets'
 import I18n from '@/context/i18n'
 import { LanguagesSupported } from '@/i18n/language'
 import { IS_CE_EDITION } from '@/config'
-import { useAppContext } from '@/context/app-context'
 import { Theme } from '@/types/app'
+import useTheme from '@/hooks/use-theme'
 
 const FILES_NUMBER_LIMIT = 20
 
@@ -226,7 +226,7 @@ const FileUploader = ({
     initialUpload(files.filter(isValid))
   }, [isValid, initialUpload])
 
-  const { theme } = useAppContext()
+  const { theme } = useTheme()
   const chartColor = useMemo(() => theme === Theme.dark ? '#5289ff' : '#296dff', [theme])
 
   useEffect(() => {

+ 2 - 2
web/app/components/header/account-setting/model-provider-page/provider-icon/index.tsx

@@ -1,12 +1,12 @@
 import type { FC } from 'react'
 import type { ModelProvider } from '../declarations'
 import { useLanguage } from '../hooks'
-import { useAppContext } from '@/context/app-context'
 import { Openai } from '@/app/components/base/icons/src/vender/other'
 import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm'
 import { renderI18nObject } from '@/hooks/use-i18n'
 import { Theme } from '@/types/app'
 import cn from '@/utils/classnames'
+import useTheme from '@/hooks/use-theme'
 
 type ProviderIconProps = {
   provider: ModelProvider
@@ -16,7 +16,7 @@ const ProviderIcon: FC<ProviderIconProps> = ({
   provider,
   className,
 }) => {
-  const { theme } = useAppContext()
+  const { theme } = useTheme()
   const language = useLanguage()
 
   if (provider.provider === 'langgenius/anthropic/anthropic') {

+ 37 - 0
web/app/components/plugins/base/badges/icon-with-tooltip.tsx

@@ -0,0 +1,37 @@
+import React, { type FC } from 'react'
+import cn from '@/utils/classnames'
+import Tooltip from '@/app/components/base/tooltip'
+import { Theme } from '@/types/app'
+
+type IconWithTooltipProps = {
+  className?: string
+  popupContent?: string
+  theme: Theme
+  BadgeIconLight: React.ElementType
+  BadgeIconDark: React.ElementType
+}
+
+const IconWithTooltip: FC<IconWithTooltipProps> = ({
+  className,
+  theme,
+  popupContent,
+  BadgeIconLight,
+  BadgeIconDark,
+}) => {
+  const isDark = theme === Theme.dark
+  const iconClassName = cn('w-5 h-5', className)
+  const Icon = isDark ? BadgeIconDark : BadgeIconLight
+
+  return (
+    <Tooltip
+      popupClassName='p-1.5 border-[0.5px] border-[0.5px] border-components-panel-border bg-components-tooltip-bg text-text-secondary system-xs-medium'
+      popupContent={popupContent}
+    >
+      <div className='flex items-center justify-center shrink-0'>
+        <Icon className={iconClassName} />
+      </div>
+    </Tooltip>
+  )
+}
+
+export default React.memo(IconWithTooltip)

+ 29 - 0
web/app/components/plugins/base/badges/partner.tsx

@@ -0,0 +1,29 @@
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import IconWithTooltip from './icon-with-tooltip'
+import PartnerDark from '@/app/components/base/icons/src/public/plugins/PartnerDark'
+import PartnerLight from '@/app/components/base/icons/src/public/plugins/PartnerLight'
+import useTheme from '@/hooks/use-theme'
+
+type PartnerProps = {
+  className?: string
+}
+
+const Partner: FC<PartnerProps> = ({
+  className,
+}) => {
+  const { t } = useTranslation()
+  const { theme } = useTheme()
+
+  return (
+    <IconWithTooltip
+      className={className}
+      theme={theme}
+      BadgeIconLight={PartnerLight}
+      BadgeIconDark={PartnerDark}
+      popupContent={t('plugin.marketplace.partnerTip')}
+    />
+  )
+}
+
+export default Partner

+ 29 - 0
web/app/components/plugins/base/badges/verified.tsx

@@ -0,0 +1,29 @@
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import IconWithTooltip from './icon-with-tooltip'
+import VerifiedDark from '@/app/components/base/icons/src/public/plugins/VerifiedDark'
+import VerifiedLight from '@/app/components/base/icons/src/public/plugins/VerifiedLight'
+import useTheme from '@/hooks/use-theme'
+
+type VerifiedProps = {
+  className?: string
+}
+
+const Verified: FC<VerifiedProps> = ({
+  className,
+}) => {
+  const { t } = useTranslation()
+  const { theme } = useTheme()
+
+  return (
+    <IconWithTooltip
+      className={className}
+      theme={theme}
+      BadgeIconLight={VerifiedLight}
+      BadgeIconDark={VerifiedDark}
+      popupContent={t('plugin.marketplace.verifiedTip')}
+    />
+  )
+}
+
+export default Verified

+ 6 - 3
web/app/components/plugins/card/index.tsx

@@ -1,6 +1,5 @@
 'use client'
 import React from 'react'
-import { RiVerifiedBadgeLine } from '@remixicon/react'
 import type { Plugin } from '../types'
 import Icon from '../card/base/card-icon'
 import CornerMark from './base/corner-mark'
@@ -14,6 +13,8 @@ import { getLanguage } from '@/i18n/language'
 import { useSingleCategories } from '../hooks'
 import { renderI18nObject } from '@/hooks/use-i18n'
 import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
+import Partner from '../base/badges/partner'
+import Verified from '../base/badges/verified'
 
 export type Props = {
   className?: string
@@ -46,11 +47,12 @@ const Card = ({
   const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
   const { t } = useMixedTranslation(localeFromProps)
   const { categoriesMap } = useSingleCategories(t)
-  const { category, type, name, org, label, brief, icon, verified } = payload
+  const { category, type, name, org, label, brief, icon, verified, badges = [] } = payload
   const isBundle = !['plugin', 'model', 'tool', 'extension', 'agent-strategy'].includes(type)
   const cornerMark = isBundle ? categoriesMap.bundle?.label : categoriesMap[category]?.label
   const getLocalizedText = (obj: Record<string, string> | undefined) =>
     obj ? renderI18nObject(obj, locale) : ''
+  const isPartner = badges.includes('partner')
 
   const wrapClassName = cn('relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)
   if (isLoading) {
@@ -71,7 +73,8 @@ const Card = ({
         <div className="ml-3 w-0 grow">
           <div className="flex items-center h-5">
             <Title title={getLocalizedText(label)} />
-            {verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />}
+            {isPartner && <Partner className='w-4 h-4 ml-0.5' />}
+            {verified && <Verified className='w-4 h-4 ml-0.5' />}
             {titleLeft} {/* This can be version badge */}
           </div>
           <OrgInfo

+ 2 - 1
web/app/components/plugins/install-plugin/utils.ts

@@ -38,7 +38,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife
     label: pluginManifest.label,
     brief: pluginManifest.brief,
     icon: pluginManifest.icon,
-    verified: pluginManifest.verified,
+    verified: true,
     introduction: pluginManifest.introduction,
     repository: '',
     install_count: 0,
@@ -46,6 +46,7 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife
       settings: [],
     },
     tags: [],
+    badges: pluginManifest.badges,
   }
 }
 

+ 2 - 0
web/app/components/plugins/types.ts

@@ -87,6 +87,7 @@ export type PluginManifestInMarket = {
   introduction: string
   verified: boolean
   install_count: number
+  badges: string[]
 }
 
 export type PluginDetail = {
@@ -137,6 +138,7 @@ export type Plugin = {
     settings: CredentialFormSchemaBase[]
   }
   tags: { name: string }[]
+  badges: string[]
 }
 
 export enum PermissionType {

+ 2 - 2
web/app/components/workflow/run/status-container.tsx

@@ -1,8 +1,8 @@
 'use client'
 import type { FC } from 'react'
-import { useAppContext } from '@/context/app-context'
 import { Theme } from '@/types/app'
 import cn from '@/utils/classnames'
+import useTheme from '@/hooks/use-theme'
 
 type Props = {
   status: string
@@ -13,7 +13,7 @@ const StatusContainer: FC<Props> = ({
   status,
   children,
 }) => {
-  const { theme } = useAppContext()
+  const { theme } = useTheme()
   return (
     <div
       className={cn(

+ 12 - 2
web/app/layout.tsx

@@ -4,6 +4,7 @@ import BrowserInitor from './components/browser-initor'
 import SentryInitor from './components/sentry-initor'
 import { getLocaleOnServer } from '@/i18n/server'
 import { TanstackQueryIniter } from '@/context/query-client'
+import { ThemeProvider } from 'next-themes'
 import './styles/globals.css'
 import './styles/markdown.scss'
 
@@ -27,7 +28,7 @@ const LocaleLayout = ({
   const locale = getLocaleOnServer()
 
   return (
-    <html lang={locale ?? 'en'} className="h-full" data-theme="light">
+    <html lang={locale ?? 'en'} className="h-full" suppressHydrationWarning>
       <head>
         <meta name="theme-color" content="#FFFFFF" />
         <meta name="mobile-web-app-capable" content="yes" />
@@ -52,7 +53,16 @@ const LocaleLayout = ({
         <BrowserInitor>
           <SentryInitor>
             <TanstackQueryIniter>
-              <I18nServer>{children}</I18nServer>
+              <ThemeProvider
+                attribute='data-theme'
+                defaultTheme='system'
+                enableSystem
+                disableTransitionOnChange
+              >
+                <I18nServer>
+                  {children}
+                </I18nServer>
+              </ThemeProvider>
             </TanstackQueryIniter>
           </SentryInitor>
         </BrowserInitor>

+ 0 - 18
web/context/app-context.tsx

@@ -8,15 +8,12 @@ import { fetchAppList } from '@/service/apps'
 import Loading from '@/app/components/base/loading'
 import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile, getSystemFeatures } from '@/service/common'
 import type { App } from '@/types/app'
-import { Theme } from '@/types/app'
 import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
 import MaintenanceNotice from '@/app/components/header/maintenance-notice'
 import type { SystemFeatures } from '@/types/feature'
 import { defaultSystemFeatures } from '@/types/feature'
 
 export type AppContextValue = {
-  theme: Theme
-  setTheme: (theme: Theme) => void
   apps: App[]
   systemFeatures: SystemFeatures
   mutateApps: VoidFunction
@@ -56,9 +53,7 @@ const initialWorkspaceInfo: ICurrentWorkspace = {
 }
 
 const AppContext = createContext<AppContextValue>({
-  theme: Theme.light,
   systemFeatures: defaultSystemFeatures,
-  setTheme: () => { },
   apps: [],
   mutateApps: () => { },
   userProfile: {
@@ -128,24 +123,11 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
       setCurrentWorkspace(currentWorkspaceResponse)
   }, [currentWorkspaceResponse])
 
-  const [theme, setTheme] = useState<Theme>(Theme.light)
-  const handleSetTheme = useCallback((theme: Theme) => {
-    setTheme(theme)
-    globalThis.document.documentElement.setAttribute('data-theme', theme)
-  }, [])
-
-  useEffect(() => {
-    globalThis.document.documentElement.setAttribute('data-theme', theme)
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [])
-
   if (!appList || !userProfile)
     return <Loading type='app' />
 
   return (
     <AppContext.Provider value={{
-      theme,
-      setTheme: handleSetTheme,
       apps: appList.data,
       systemFeatures: { ...defaultSystemFeatures, ...systemFeatures },
       mutateApps,

+ 13 - 0
web/hooks/use-theme.ts

@@ -0,0 +1,13 @@
+import { Theme } from '@/types/app'
+import { useTheme as useBaseTheme } from 'next-themes'
+
+const useTheme = () => {
+  const { theme, resolvedTheme, ...rest } = useBaseTheme()
+  return {
+    // only returns 'light' or 'dark' theme
+    theme: theme === Theme.system ? resolvedTheme as Theme : theme as Theme,
+    ...rest,
+  }
+}
+
+export default useTheme

+ 2 - 0
web/i18n/en-US/plugin.ts

@@ -194,6 +194,8 @@ const translation = {
       firstReleased: 'First Released',
     },
     viewMore: 'View more',
+    verifiedTip: 'This plugin is verified by Dify',
+    partnerTip: 'This plugin is developed by Dify partners',
   },
   task: {
     installing: 'Installing {{installingLength}} plugins, 0 done.',

+ 2 - 0
web/i18n/zh-Hans/plugin.ts

@@ -194,6 +194,8 @@ const translation = {
       firstReleased: '首次发布',
     },
     viewMore: '查看更多',
+    verifiedTip: '此插件已由 Dify 认证',
+    partnerTip: '此插件由 Dify 合作伙伴开发',
   },
   task: {
     installing: '{{installingLength}} 个插件安装中,0 已完成',

+ 7 - 0
web/package.json

@@ -33,7 +33,13 @@
     "@headlessui/react": "^1.7.13",
     "@heroicons/react": "^2.0.16",
     "@hookform/resolvers": "^3.9.0",
+    "@lexical/code": "^0.18.0",
+    "@lexical/link": "^0.18.0",
+    "@lexical/list": "^0.18.0",
     "@lexical/react": "^0.18.0",
+    "@lexical/selection": "^0.18.0",
+    "@lexical/text": "^0.18.0",
+    "@lexical/utils": "^0.18.0",
     "@mdx-js/loader": "^3.1.0",
     "@mdx-js/react": "^3.1.0",
     "@monaco-editor/react": "^4.6.0",
@@ -77,6 +83,7 @@
     "mitt": "^3.0.1",
     "negotiator": "^0.6.3",
     "next": "^14.2.10",
+    "next-themes": "^0.4.3",
     "pinyin-pro": "^3.25.0",
     "qrcode.react": "^4.1.0",
     "qs": "^6.13.0",

+ 32 - 0
web/pnpm-lock.yaml

@@ -40,9 +40,27 @@ importers:
       '@hookform/resolvers':
         specifier: ^3.9.0
         version: 3.9.0(react-hook-form@7.53.1(react@18.2.0))
+      '@lexical/code':
+        specifier: ^0.18.0
+        version: 0.18.0
+      '@lexical/link':
+        specifier: ^0.18.0
+        version: 0.18.0
+      '@lexical/list':
+        specifier: ^0.18.0
+        version: 0.18.0
       '@lexical/react':
         specifier: ^0.18.0
         version: 0.18.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(yjs@13.6.20)
+      '@lexical/selection':
+        specifier: ^0.18.0
+        version: 0.18.0
+      '@lexical/text':
+        specifier: ^0.18.0
+        version: 0.18.0
+      '@lexical/utils':
+        specifier: ^0.18.0
+        version: 0.18.0
       '@mdx-js/loader':
         specifier: ^3.1.0
         version: 3.1.0(acorn@8.13.0)(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3))
@@ -172,6 +190,9 @@ importers:
       next:
         specifier: ^14.2.10
         version: 14.2.15(@babel/core@7.25.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.80.3)
+      next-themes:
+        specifier: ^0.4.3
+        version: 0.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
       pinyin-pro:
         specifier: ^3.25.0
         version: 3.25.0
@@ -6299,6 +6320,12 @@ packages:
   neo-async@2.6.2:
     resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
 
+  next-themes@0.4.4:
+    resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
+    peerDependencies:
+      react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
   next@14.2.15:
     resolution: {integrity: sha512-h9ctmOokpoDphRvMGnwOJAedT6zKhwqyZML9mDtspgf4Rh3Pn7UTYKqePNoDvhsWBAO5GoPNYshnAUGIazVGmw==}
     engines: {node: '>=18.17.0'}
@@ -15966,6 +15993,11 @@ snapshots:
 
   neo-async@2.6.2: {}
 
+  next-themes@0.4.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+    dependencies:
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+
   next@14.2.15(@babel/core@7.25.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.80.3):
     dependencies:
       '@next/env': 14.2.15

+ 1 - 0
web/types/app.ts

@@ -11,6 +11,7 @@ import type { UploadFileSetting } from '@/app/components/workflow/types'
 export enum Theme {
   light = 'light',
   dark = 'dark',
+  system = 'system',
 }
 
 export enum ProviderType {