Joel преди 10 месеца
родител
ревизия
8fa6cb5e03
променени са 34 файла, в които са добавени 1810 реда и са изтрити 14 реда
  1. 2 10
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx
  2. 87 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx
  3. 179 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx
  4. 6 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts
  5. 41 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx
  6. 227 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx
  7. 292 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx
  8. 77 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx
  9. 46 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx
  10. 28 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx
  11. 16 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts
  12. 9 0
      web/app/components/base/icons/assets/public/tracing/langfuse-icon-big.svg
  13. 9 0
      web/app/components/base/icons/assets/public/tracing/langfuse-icon.svg
  14. 19 0
      web/app/components/base/icons/assets/public/tracing/langsmith-icon-big.svg
  15. 19 0
      web/app/components/base/icons/assets/public/tracing/langsmith-icon.svg
  16. 6 0
      web/app/components/base/icons/assets/public/tracing/tracing-icon.svg
  17. 73 0
      web/app/components/base/icons/src/public/tracing/LangfuseIcon.json
  18. 16 0
      web/app/components/base/icons/src/public/tracing/LangfuseIcon.tsx
  19. 73 0
      web/app/components/base/icons/src/public/tracing/LangfuseIconBig.json
  20. 16 0
      web/app/components/base/icons/src/public/tracing/LangfuseIconBig.tsx
  21. 173 0
      web/app/components/base/icons/src/public/tracing/LangsmithIcon.json
  22. 16 0
      web/app/components/base/icons/src/public/tracing/LangsmithIcon.tsx
  23. 173 0
      web/app/components/base/icons/src/public/tracing/LangsmithIconBig.json
  24. 16 0
      web/app/components/base/icons/src/public/tracing/LangsmithIconBig.tsx
  25. 47 0
      web/app/components/base/icons/src/public/tracing/TracingIcon.json
  26. 16 0
      web/app/components/base/icons/src/public/tracing/TracingIcon.tsx
  27. 5 0
      web/app/components/base/icons/src/public/tracing/index.ts
  28. 4 0
      web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx
  29. 36 0
      web/i18n/en-US/app.ts
  30. 2 1
      web/i18n/en-US/common.ts
  31. 36 0
      web/i18n/zh-Hans/app.ts
  32. 2 1
      web/i18n/zh-Hans/common.ts
  33. 11 0
      web/models/app.ts
  34. 32 2
      web/service/apps.ts

+ 2 - 10
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/page.tsx

@@ -1,7 +1,7 @@
 import React from 'react'
 import ChartView from './chartView'
 import CardView from './cardView'
-import { getLocaleOnServer, useTranslation as translate } from '@/i18n/server'
+import TracingPanel from './tracing/panel'
 import ApikeyInfoPanel from '@/app/components/app/overview/apikey-info-panel'
 
 export type IDevelopProps = {
@@ -11,18 +11,10 @@ export type IDevelopProps = {
 const Overview = async ({
   params: { appId },
 }: IDevelopProps) => {
-  const locale = getLocaleOnServer()
-  /*
-    rename useTranslation to avoid lint error
-    please check: https://github.com/i18next/next-13-app-dir-i18next-example/issues/24
-  */
-  const { t } = await translate(locale, 'app-overview')
   return (
     <div className="h-full px-4 sm:px-16 py-6 overflow-scroll">
       <ApikeyInfoPanel />
-      <div className='flex flex-row items-center justify-between mb-4 text-xl text-gray-900'>
-        {t('overview.title')}
-      </div>
+      <TracingPanel />
       <CardView appId={appId} />
       <ChartView appId={appId} />
     </div>

+ 87 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx

@@ -0,0 +1,87 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import type { PopupProps } from './config-popup'
+import ConfigPopup from './config-popup'
+import Button from '@/app/components/base/button'
+import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+
+const I18N_PREFIX = 'app.tracing'
+
+type Props = {
+  readOnly: boolean
+  className?: string
+  hasConfigured: boolean
+  controlShowPopup?: number
+} & PopupProps
+
+const ConfigBtn: FC<Props> = ({
+  className,
+  hasConfigured,
+  controlShowPopup,
+  ...popupProps
+}) => {
+  const { t } = useTranslation()
+  const [open, doSetOpen] = useState(false)
+  const openRef = useRef(open)
+  const setOpen = useCallback((v: boolean) => {
+    doSetOpen(v)
+    openRef.current = v
+  }, [doSetOpen])
+
+  const handleTrigger = useCallback(() => {
+    setOpen(!openRef.current)
+  }, [setOpen])
+
+  useEffect(() => {
+    if (controlShowPopup)
+      // setOpen(!openRef.current)
+      setOpen(true)
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [controlShowPopup])
+
+  if (popupProps.readOnly && !hasConfigured)
+    return null
+
+  const triggerContent = hasConfigured
+    ? (
+      <div className={cn(className, 'p-1 rounded-md hover:bg-black/5 cursor-pointer')}>
+        <Settings04 className='w-4 h-4 text-gray-500' />
+      </div>
+    )
+    : (
+      <Button variant='primary'
+        className={cn(className, '!h-8 !px-3 select-none')}
+      >
+        <Settings04 className='mr-1 w-4 h-4' />
+        <span className='text-[13px]'>{t(`${I18N_PREFIX}.config`)}</span>
+      </Button>
+    )
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={{
+        mainAxis: 12,
+        crossAxis: hasConfigured ? 8 : 0,
+      }}
+    >
+      <PortalToFollowElemTrigger onClick={handleTrigger}>
+        {triggerContent}
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className='z-[11]'>
+        <ConfigPopup {...popupProps} />
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+export default React.memo(ConfigBtn)

+ 179 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx

@@ -0,0 +1,179 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useBoolean } from 'ahooks'
+import TracingIcon from './tracing-icon'
+import ProviderPanel from './provider-panel'
+import type { LangFuseConfig, LangSmithConfig } from './type'
+import { TracingProvider } from './type'
+import ProviderConfigModal from './provider-config-modal'
+import Indicator from '@/app/components/header/indicator'
+import Switch from '@/app/components/base/switch'
+import TooltipPlus from '@/app/components/base/tooltip-plus'
+
+const I18N_PREFIX = 'app.tracing'
+
+export type PopupProps = {
+  appId: string
+  readOnly: boolean
+  enabled: boolean
+  onStatusChange: (enabled: boolean) => void
+  chosenProvider: TracingProvider | null
+  onChooseProvider: (provider: TracingProvider) => void
+  langSmithConfig: LangSmithConfig | null
+  langFuseConfig: LangFuseConfig | null
+  onConfigUpdated: (provider: TracingProvider, payload: LangSmithConfig | LangFuseConfig) => void
+  onConfigRemoved: (provider: TracingProvider) => void
+}
+
+const ConfigPopup: FC<PopupProps> = ({
+  appId,
+  readOnly,
+  enabled,
+  onStatusChange,
+  chosenProvider,
+  onChooseProvider,
+  langSmithConfig,
+  langFuseConfig,
+  onConfigUpdated,
+  onConfigRemoved,
+}) => {
+  const { t } = useTranslation()
+
+  const [currentProvider, setCurrentProvider] = useState<TracingProvider | null>(TracingProvider.langfuse)
+  const [isShowConfigModal, {
+    setTrue: showConfigModal,
+    setFalse: hideConfigModal,
+  }] = useBoolean(false)
+  const handleOnConfig = useCallback((provider: TracingProvider) => {
+    return () => {
+      setCurrentProvider(provider)
+      showConfigModal()
+    }
+  }, [showConfigModal])
+
+  const handleOnChoose = useCallback((provider: TracingProvider) => {
+    return () => {
+      onChooseProvider(provider)
+    }
+  }, [onChooseProvider])
+
+  const handleConfigUpdated = useCallback((payload: LangSmithConfig | LangFuseConfig) => {
+    onConfigUpdated(currentProvider!, payload)
+    hideConfigModal()
+  }, [currentProvider, hideConfigModal, onConfigUpdated])
+
+  const handleConfigRemoved = useCallback(() => {
+    onConfigRemoved(currentProvider!)
+    hideConfigModal()
+  }, [currentProvider, hideConfigModal, onConfigRemoved])
+
+  const providerAllConfigured = langSmithConfig && langFuseConfig
+  const providerAllNotConfigured = !langSmithConfig && !langFuseConfig
+
+  const switchContent = (
+    <Switch
+      className='ml-3'
+      defaultValue={enabled}
+      onChange={onStatusChange}
+      size='l'
+      disabled={providerAllNotConfigured}
+    />
+  )
+  const langSmithPanel = (
+    <ProviderPanel
+      type={TracingProvider.langSmith}
+      readOnly={readOnly}
+      hasConfigured={!!langSmithConfig}
+      onConfig={handleOnConfig(TracingProvider.langSmith)}
+      isChosen={chosenProvider === TracingProvider.langSmith}
+      onChoose={handleOnChoose(TracingProvider.langSmith)}
+    />
+  )
+
+  const langfusePanel = (
+    <ProviderPanel
+      type={TracingProvider.langfuse}
+      readOnly={readOnly}
+      hasConfigured={!!langFuseConfig}
+      onConfig={handleOnConfig(TracingProvider.langfuse)}
+      isChosen={chosenProvider === TracingProvider.langfuse}
+      onChoose={handleOnChoose(TracingProvider.langfuse)}
+    />
+  )
+
+  return (
+    <div className='w-[420px] p-4 rounded-2xl bg-white border-[0.5px] border-black/5 shadow-lg'>
+      <div className='flex justify-between items-center'>
+        <div className='flex items-center'>
+          <TracingIcon size='md' className='mr-2' />
+          <div className='leading-[120%] text-[18px] font-semibold text-gray-900'>{t(`${I18N_PREFIX}.tracing`)}</div>
+        </div>
+        <div className='flex items-center'>
+          <Indicator color={enabled ? 'green' : 'gray'} />
+          <div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'>
+            {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
+          </div>
+          {!readOnly && (
+            <>
+              {providerAllNotConfigured
+                ? (
+                  <TooltipPlus
+                    popupContent={t(`${I18N_PREFIX}.disabledTip`)}
+                  >
+                    {switchContent}
+
+                  </TooltipPlus>
+                )
+                : switchContent}
+            </>
+          )}
+
+        </div>
+      </div>
+
+      <div className='mt-2 leading-4 text-xs font-normal text-gray-500'>
+        {t(`${I18N_PREFIX}.tracingDescription`)}
+      </div>
+      <div className='mt-3 h-px bg-gray-100'></div>
+      <div className='mt-3'>
+        {(providerAllConfigured || providerAllNotConfigured)
+          ? (
+            <>
+              <div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.${providerAllConfigured ? 'configured' : 'notConfigured'}`)}</div>
+              <div className='mt-2 space-y-2'>
+                {langSmithPanel}
+                {langfusePanel}
+              </div>
+            </>
+          )
+          : (
+            <>
+              <div className='leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.configured`)}</div>
+              <div className='mt-2'>
+                {langSmithConfig ? langSmithPanel : langfusePanel}
+              </div>
+              <div className='mt-3 leading-4 text-xs font-medium text-gray-500 uppercase'>{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`)}</div>
+              <div className='mt-2'>
+                {!langSmithConfig ? langSmithPanel : langfusePanel}
+              </div>
+            </>
+          )}
+
+      </div>
+      {isShowConfigModal && (
+        <ProviderConfigModal
+          appId={appId}
+          type={currentProvider!}
+          payload={currentProvider === TracingProvider.langSmith ? langSmithConfig : langFuseConfig}
+          onCancel={hideConfigModal}
+          onSaved={handleConfigUpdated}
+          onChosen={onChooseProvider}
+          onRemoved={handleConfigRemoved}
+        />
+      )}
+    </div>
+  )
+}
+export default React.memo(ConfigPopup)

+ 6 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config.ts

@@ -0,0 +1,6 @@
+import { TracingProvider } from './type'
+
+export const docURL = {
+  [TracingProvider.langSmith]: 'https://docs.smith.langchain.com/',
+  [TracingProvider.langfuse]: 'https://docs.langfuse.com',
+}

+ 41 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx

@@ -0,0 +1,41 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import cn from 'classnames'
+
+type Props = {
+  className?: string
+  label: string
+  labelClassName?: string
+  value: string | number
+  onChange: (value: string) => void
+  isRequired?: boolean
+  placeholder?: string
+}
+
+const Field: FC<Props> = ({
+  className,
+  label,
+  labelClassName,
+  value,
+  onChange,
+  isRequired = false,
+  placeholder = '',
+}) => {
+  return (
+    <div className={cn(className)}>
+      <div className='flex py-[7px]'>
+        <div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div>
+        {isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
+      </div>
+      <input
+        type='text'
+        value={value}
+        onChange={e => onChange(e.target.value)}
+        className='flex h-9 w-full py-1 px-2 rounded-lg text-xs leading-normal bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-gray-50 placeholder:text-gray-400'
+        placeholder={placeholder}
+      />
+    </div>
+  )
+}
+export default React.memo(Field)

+ 227 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx

@@ -0,0 +1,227 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import { usePathname } from 'next/navigation'
+import { useBoolean } from 'ahooks'
+import type { LangFuseConfig, LangSmithConfig } from './type'
+import { TracingProvider } from './type'
+import TracingIcon from './tracing-icon'
+import ToggleExpandBtn from './toggle-fold-btn'
+import ConfigButton from './config-button'
+import { LangfuseIcon, LangsmithIcon } from '@/app/components/base/icons/src/public/tracing'
+import Indicator from '@/app/components/header/indicator'
+import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps'
+import type { TracingStatus } from '@/models/app'
+import Toast from '@/app/components/base/toast'
+import { useAppContext } from '@/context/app-context'
+import Loading from '@/app/components/base/loading'
+
+const I18N_PREFIX = 'app.tracing'
+
+const Title = ({
+  className,
+}: {
+  className?: string
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <div className={cn(className, 'flex items-center text-lg font-semibold text-gray-900')}>
+      {t('common.appMenus.overview')}
+    </div>
+  )
+}
+const Panel: FC = () => {
+  const { t } = useTranslation()
+  const pathname = usePathname()
+  const matched = pathname.match(/\/app\/([^/]+)/)
+  const appId = (matched?.length && matched[1]) ? matched[1] : ''
+  const { isCurrentWorkspaceEditor } = useAppContext()
+  const readOnly = !isCurrentWorkspaceEditor
+
+  const [isLoaded, {
+    setTrue: setLoaded,
+  }] = useBoolean(false)
+
+  const [tracingStatus, setTracingStatus] = useState<TracingStatus | null>(null)
+  const enabled = tracingStatus?.enabled || false
+  const handleTracingStatusChange = async (tracingStatus: TracingStatus, noToast?: boolean) => {
+    await updateTracingStatus({ appId, body: tracingStatus })
+    setTracingStatus(tracingStatus)
+    if (!noToast) {
+      Toast.notify({
+        type: 'success',
+        message: t('common.api.success'),
+      })
+    }
+  }
+
+  const handleTracingEnabledChange = (enabled: boolean) => {
+    handleTracingStatusChange({
+      tracing_provider: tracingStatus?.tracing_provider || null,
+      enabled,
+    })
+  }
+  const handleChooseProvider = (provider: TracingProvider) => {
+    handleTracingStatusChange({
+      tracing_provider: provider,
+      enabled: true,
+    })
+  }
+  const inUseTracingProvider: TracingProvider | null = tracingStatus?.tracing_provider || null
+  const InUseProviderIcon = inUseTracingProvider === TracingProvider.langSmith ? LangsmithIcon : LangfuseIcon
+
+  const [langSmithConfig, setLangSmithConfig] = useState<LangSmithConfig | null>(null)
+  const [langFuseConfig, setLangFuseConfig] = useState<LangFuseConfig | null>(null)
+  const hasConfiguredTracing = !!(langSmithConfig || langFuseConfig)
+
+  const fetchTracingConfig = async () => {
+    const { tracing_config: langSmithConfig, has_not_configured: langSmithHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langSmith })
+    if (!langSmithHasNotConfig)
+      setLangSmithConfig(langSmithConfig as LangSmithConfig)
+    const { tracing_config: langFuseConfig, has_not_configured: langFuseHasNotConfig } = await doFetchTracingConfig({ appId, provider: TracingProvider.langfuse })
+    if (!langFuseHasNotConfig)
+      setLangFuseConfig(langFuseConfig as LangFuseConfig)
+  }
+
+  const handleTracingConfigUpdated = async (provider: TracingProvider) => {
+    // call api to hide secret key value
+    const { tracing_config } = await doFetchTracingConfig({ appId, provider })
+    if (provider === TracingProvider.langSmith)
+      setLangSmithConfig(tracing_config as LangSmithConfig)
+    else
+      setLangFuseConfig(tracing_config as LangFuseConfig)
+  }
+
+  const handleTracingConfigRemoved = (provider: TracingProvider) => {
+    if (provider === TracingProvider.langSmith)
+      setLangSmithConfig(null)
+    else
+      setLangFuseConfig(null)
+    if (provider === inUseTracingProvider) {
+      handleTracingStatusChange({
+        enabled: false,
+        tracing_provider: null,
+      }, true)
+    }
+  }
+
+  useEffect(() => {
+    (async () => {
+      const tracingStatus = await fetchTracingStatus({ appId })
+      setTracingStatus(tracingStatus)
+      await fetchTracingConfig()
+      setLoaded()
+    })()
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  const [isFold, setFold] = useState(false)
+  const [controlShowPopup, setControlShowPopup] = useState<number>(0)
+  const showPopup = useCallback(() => {
+    setControlShowPopup(Date.now())
+  }, [setControlShowPopup])
+  if (!isLoaded) {
+    return (
+      <div className='flex items-center justify-between mb-3'>
+        <Title className='h-[41px]' />
+        <div className='w-[200px]'>
+          <Loading />
+        </div>
+      </div>
+    )
+  }
+
+  if (!isFold && !hasConfiguredTracing) {
+    return (
+      <div className={cn('mb-3')}>
+        <Title />
+        <div className='mt-2 flex justify-between p-3 pr-4 items-center bg-white border-[0.5px] border-black/8 rounded-xl shadow-md'>
+          <div className='flex space-x-2'>
+            <TracingIcon size='lg' className='m-1' />
+            <div>
+              <div className='mb-0.5 leading-6 text-base font-semibold text-gray-900'>{t(`${I18N_PREFIX}.title`)}</div>
+              <div className='flex justify-between leading-4 text-xs font-normal text-gray-500'>
+                <span className='mr-2'>{t(`${I18N_PREFIX}.description`)}</span>
+                <div className='flex space-x-3'>
+                  <LangsmithIcon className='h-4' />
+                  <LangfuseIcon className='h-4' />
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div className='flex items-center space-x-1'>
+            <ConfigButton
+              appId={appId}
+              readOnly={readOnly}
+              hasConfigured={false}
+              enabled={enabled}
+              onStatusChange={handleTracingEnabledChange}
+              chosenProvider={inUseTracingProvider}
+              onChooseProvider={handleChooseProvider}
+              langSmithConfig={langSmithConfig}
+              langFuseConfig={langFuseConfig}
+              onConfigUpdated={handleTracingConfigUpdated}
+              onConfigRemoved={handleTracingConfigRemoved}
+            />
+            <ToggleExpandBtn isFold={isFold} onFoldChange={setFold} />
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  return (
+    <div className={cn('mb-3 flex justify-between items-center cursor-pointer')} onClick={showPopup}>
+      <Title className='h-[41px]' />
+      <div className='flex items-center p-2 rounded-xl border-[0.5px] border-gray-200 shadow-xs hover:bg-gray-100'>
+        {!inUseTracingProvider
+          ? <>
+            <TracingIcon size='md' className='mr-2' />
+            <div className='leading-5 text-sm font-semibold text-gray-700'>{t(`${I18N_PREFIX}.title`)}</div>
+          </>
+          : <InUseProviderIcon className='ml-1 h-4' />}
+
+        {hasConfiguredTracing && (
+          <div className='ml-4 mr-1 flex items-center'>
+            <Indicator color={enabled ? 'green' : 'gray'} />
+            <div className='ml-1.5 text-xs font-semibold text-gray-500 uppercase'>
+              {t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
+            </div>
+          </div>
+        )}
+
+        {hasConfiguredTracing && (
+          <div className='ml-2 w-px h-3.5 bg-gray-200'></div>
+        )}
+        <div className='flex items-center' onClick={e => e.stopPropagation()}>
+          <ConfigButton
+            appId={appId}
+            readOnly={readOnly}
+            hasConfigured
+            className='ml-2'
+            enabled={enabled}
+            onStatusChange={handleTracingEnabledChange}
+            chosenProvider={inUseTracingProvider}
+            onChooseProvider={handleChooseProvider}
+            langSmithConfig={langSmithConfig}
+            langFuseConfig={langFuseConfig}
+            onConfigUpdated={handleTracingConfigUpdated}
+            onConfigRemoved={handleTracingConfigRemoved}
+            controlShowPopup={controlShowPopup}
+          />
+        </div>
+        {!hasConfiguredTracing && (
+          <div className='flex items-center' onClick={e => e.stopPropagation()}>
+            <div className='mx-2 w-px h-3.5 bg-gray-200'></div>
+            <ToggleExpandBtn isFold={isFold} onFoldChange={setFold} />
+          </div>
+        )}
+      </div>
+    </div>
+  )
+}
+export default React.memo(Panel)

+ 292 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-config-modal.tsx

@@ -0,0 +1,292 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useBoolean } from 'ahooks'
+import Field from './field'
+import type { LangFuseConfig, LangSmithConfig } from './type'
+import { TracingProvider } from './type'
+import { docURL } from './config'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+} from '@/app/components/base/portal-to-follow-elem'
+import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
+import Button from '@/app/components/base/button'
+import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
+import ConfirmUi from '@/app/components/base/confirm'
+import { addTracingConfig, removeTracingConfig, updateTracingConfig } from '@/service/apps'
+import Toast from '@/app/components/base/toast'
+
+type Props = {
+  appId: string
+  type: TracingProvider
+  payload?: LangSmithConfig | LangFuseConfig | null
+  onRemoved: () => void
+  onCancel: () => void
+  onSaved: (payload: LangSmithConfig | LangFuseConfig) => void
+  onChosen: (provider: TracingProvider) => void
+}
+
+const I18N_PREFIX = 'app.tracing.configProvider'
+
+const langSmithConfigTemplate = {
+  api_key: '',
+  project: '',
+  endpoint: '',
+}
+
+const langFuseConfigTemplate = {
+  public_key: '',
+  secret_key: '',
+  host: '',
+}
+
+const ProviderConfigModal: FC<Props> = ({
+  appId,
+  type,
+  payload,
+  onRemoved,
+  onCancel,
+  onSaved,
+  onChosen,
+}) => {
+  const { t } = useTranslation()
+  const isEdit = !!payload
+  const isAdd = !isEdit
+  const [isSaving, setIsSaving] = useState(false)
+  const [config, setConfig] = useState<LangSmithConfig | LangFuseConfig>((() => {
+    if (isEdit)
+      return payload
+
+    if (type === TracingProvider.langSmith)
+      return langSmithConfigTemplate
+
+    return langFuseConfigTemplate
+  })())
+  const [isShowRemoveConfirm, {
+    setTrue: showRemoveConfirm,
+    setFalse: hideRemoveConfirm,
+  }] = useBoolean(false)
+
+  const handleRemove = useCallback(async () => {
+    await removeTracingConfig({
+      appId,
+      provider: type,
+    })
+    Toast.notify({
+      type: 'success',
+      message: t('common.api.remove'),
+    })
+    onRemoved()
+    hideRemoveConfirm()
+  }, [hideRemoveConfirm, appId, type, t, onRemoved])
+
+  const handleConfigChange = useCallback((key: string) => {
+    return (value: string) => {
+      setConfig({
+        ...config,
+        [key]: value,
+      })
+    }
+  }, [config])
+
+  const checkValid = useCallback(() => {
+    let errorMessage = ''
+    if (type === TracingProvider.langSmith) {
+      const postData = config as LangSmithConfig
+      if (!postData.api_key)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: 'API Key' })
+      if (!errorMessage && !postData.project)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.project`) })
+    }
+
+    if (type === TracingProvider.langfuse) {
+      const postData = config as LangFuseConfig
+      if (!errorMessage && !postData.secret_key)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.secretKey`) })
+      if (!errorMessage && !postData.public_key)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: t(`${I18N_PREFIX}.publicKey`) })
+      if (!errorMessage && !postData.host)
+        errorMessage = t('common.errorMsg.fieldRequired', { field: 'Host' })
+    }
+
+    return errorMessage
+  }, [config, t, type])
+  const handleSave = useCallback(async () => {
+    if (isSaving)
+      return
+    const errorMessage = checkValid()
+    if (errorMessage) {
+      Toast.notify({
+        type: 'error',
+        message: errorMessage,
+      })
+      return
+    }
+    const action = isEdit ? updateTracingConfig : addTracingConfig
+    try {
+      await action({
+        appId,
+        body: {
+          tracing_provider: type,
+          tracing_config: config,
+        },
+      })
+      Toast.notify({
+        type: 'success',
+        message: t('common.api.success'),
+      })
+      onSaved(config)
+      if (isAdd)
+        onChosen(type)
+    }
+    finally {
+      setIsSaving(false)
+    }
+  }, [appId, checkValid, config, isAdd, isEdit, isSaving, onChosen, onSaved, t, type])
+
+  return (
+    <>
+      {!isShowRemoveConfirm
+        ? (
+          <PortalToFollowElem open>
+            <PortalToFollowElemContent className='w-full h-full z-[60]'>
+              <div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
+                <div className='mx-2 w-[640px] max-h-[calc(100vh-120px)] bg-white shadow-xl rounded-2xl overflow-y-auto'>
+                  <div className='px-8 pt-8'>
+                    <div className='flex justify-between items-center mb-4'>
+                      <div className='text-xl font-semibold text-gray-900'>{t(`${I18N_PREFIX}.title`)}{t(`app.tracing.${type}.title`)}</div>
+                    </div>
+
+                    <div className='space-y-4'>
+                      {type === TracingProvider.langSmith && (
+                        <>
+                          <Field
+                            label='API Key'
+                            labelClassName='!text-sm'
+                            isRequired
+                            value={(config as LangSmithConfig).api_key}
+                            onChange={handleConfigChange('api_key')}
+                            placeholder={t(`${I18N_PREFIX}.placeholder`, { key: 'API Key' })!}
+                          />
+                          <Field
+                            label={t(`${I18N_PREFIX}.project`)!}
+                            labelClassName='!text-sm'
+                            isRequired
+                            value={(config as LangSmithConfig).project}
+                            onChange={handleConfigChange('project')}
+                            placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.project`) })!}
+                          />
+                          <Field
+                            label='Endpoint'
+                            labelClassName='!text-sm'
+                            value={(config as LangSmithConfig).endpoint}
+                            onChange={handleConfigChange('endpoint')}
+                            placeholder={'https://api.smith.langchain.com'}
+                          />
+                        </>
+                      )}
+                      {type === TracingProvider.langfuse && (
+                        <>
+                          <Field
+                            label={t(`${I18N_PREFIX}.secretKey`)!}
+                            labelClassName='!text-sm'
+                            value={(config as LangFuseConfig).secret_key}
+                            isRequired
+                            onChange={handleConfigChange('secret_key')}
+                            placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.secretKey`) })!}
+                          />
+                          <Field
+                            label={t(`${I18N_PREFIX}.publicKey`)!}
+                            labelClassName='!text-sm'
+                            isRequired
+                            value={(config as LangFuseConfig).public_key}
+                            onChange={handleConfigChange('public_key')}
+                            placeholder={t(`${I18N_PREFIX}.placeholder`, { key: t(`${I18N_PREFIX}.publicKey`) })!}
+                          />
+                          <Field
+                            label='Host'
+                            labelClassName='!text-sm'
+                            isRequired
+                            value={(config as LangFuseConfig).host}
+                            onChange={handleConfigChange('host')}
+                            placeholder='https://cloud.langfuse.com'
+                          />
+                        </>
+                      )}
+
+                    </div>
+                    <div className='my-8 flex justify-between items-center h-8'>
+                      <a
+                        className='flex items-center space-x-1 leading-[18px] text-xs font-normal text-[#155EEF]'
+                        target='_blank'
+                        href={docURL[type]}
+                      >
+                        <span>{t(`${I18N_PREFIX}.viewDocsLink`, { key: t(`app.tracing.${type}.title`) })}</span>
+                        <LinkExternal02 className='w-3 h-3' />
+                      </a>
+                      <div className='flex items-center'>
+                        {isEdit && (
+                          <>
+                            <Button
+                              className='h-9 text-sm font-medium text-gray-700'
+                              onClick={showRemoveConfirm}
+                            >
+                              <span className='text-[#D92D20]'>{t('common.operation.remove')}</span>
+                            </Button>
+                            <div className='mx-3 w-px h-[18px] bg-gray-200'></div>
+                          </>
+                        )}
+                        <Button
+                          className='mr-2 h-9 text-sm font-medium text-gray-700'
+                          onClick={onCancel}
+                        >
+                          {t('common.operation.cancel')}
+                        </Button>
+                        <Button
+                          className='h-9 text-sm font-medium'
+                          variant='primary'
+                          onClick={handleSave}
+                          loading={isSaving}
+                        >
+                          {t(`common.operation.${isAdd ? 'saveAndEnable' : 'save'}`)}
+                        </Button>
+                      </div>
+
+                    </div>
+                  </div>
+                  <div className='border-t-[0.5px] border-t-black/5'>
+                    <div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
+                      <Lock01 className='mr-1 w-3 h-3 text-gray-500' />
+                      {t('common.modelProvider.encrypted.front')}
+                      <a
+                        className='text-primary-600 mx-1'
+                        target='_blank' rel='noopener noreferrer'
+                        href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
+                      >
+                        PKCS1_OAEP
+                      </a>
+                      {t('common.modelProvider.encrypted.back')}
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </PortalToFollowElemContent>
+          </PortalToFollowElem>
+        )
+        : (
+          <ConfirmUi
+            isShow
+            onClose={hideRemoveConfirm}
+            type='warning'
+            title={t(`${I18N_PREFIX}.removeConfirmTitle`, { key: t(`app.tracing.${type}.title`) })!}
+            content={t(`${I18N_PREFIX}.removeConfirmContent`)}
+            onConfirm={handleRemove}
+            onCancel={hideRemoveConfirm}
+          />
+        )}
+    </>
+  )
+}
+export default React.memo(ProviderConfigModal)

+ 77 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx

@@ -0,0 +1,77 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import { TracingProvider } from './type'
+import { LangfuseIconBig, LangsmithIconBig } from '@/app/components/base/icons/src/public/tracing'
+import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
+
+const I18N_PREFIX = 'app.tracing'
+
+type Props = {
+  type: TracingProvider
+  readOnly: boolean
+  isChosen: boolean
+  onChoose: () => void
+  hasConfigured: boolean
+  onConfig: () => void
+}
+
+const getIcon = (type: TracingProvider) => {
+  return ({
+    [TracingProvider.langSmith]: LangsmithIconBig,
+    [TracingProvider.langfuse]: LangfuseIconBig,
+  })[type]
+}
+
+const ProviderPanel: FC<Props> = ({
+  type,
+  readOnly,
+  isChosen,
+  onChoose,
+  hasConfigured,
+  onConfig,
+}) => {
+  const { t } = useTranslation()
+  const Icon = getIcon(type)
+
+  const handleConfigBtnClick = useCallback((e: React.MouseEvent) => {
+    e.stopPropagation()
+    onConfig()
+  }, [onConfig])
+
+  const handleChosen = useCallback((e: React.MouseEvent) => {
+    e.stopPropagation()
+    if (isChosen || !hasConfigured || readOnly)
+      return
+    onChoose()
+  }, [hasConfigured, isChosen, onChoose, readOnly])
+  return (
+    <div
+      className={cn(isChosen ? 'border-primary-400' : 'border-transparent', !isChosen && hasConfigured && !readOnly && 'cursor-pointer', 'px-4 py-3 rounded-xl border-[1.5px]  bg-gray-100')}
+      onClick={handleChosen}
+    >
+      <div className={'flex justify-between items-center space-x-1'}>
+        <div className='flex items-center'>
+          <Icon className='h-6' />
+          {isChosen && <div className='ml-1 flex items-center h-4  px-1 rounded-[4px] border border-primary-500 leading-4 text-xs font-medium text-primary-500 uppercase '>{t(`${I18N_PREFIX}.inUse`)}</div>}
+        </div>
+        {!readOnly && (
+          <div
+            className='flex px-2 items-center h-6 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer text-gray-700 space-x-1'
+            onClick={handleConfigBtnClick}
+          >
+            <Settings04 className='w-3 h-3' />
+            <div className='text-xs font-medium'>{t(`${I18N_PREFIX}.config`)}</div>
+          </div>
+        )}
+
+      </div>
+      <div className='mt-2 leading-4 text-xs font-normal text-gray-500'>
+        {t(`${I18N_PREFIX}.${type}.description`)}
+      </div>
+    </div>
+  )
+}
+export default React.memo(ProviderPanel)

+ 46 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/toggle-fold-btn.tsx

@@ -0,0 +1,46 @@
+'use client'
+import { ChevronDoubleDownIcon } from '@heroicons/react/20/solid'
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import React, { useCallback } from 'react'
+import TooltipPlus from '@/app/components/base/tooltip-plus'
+
+const I18N_PREFIX = 'app.tracing'
+
+type Props = {
+  isFold: boolean
+  onFoldChange: (isFold: boolean) => void
+}
+
+const ToggleFoldBtn: FC<Props> = ({
+  isFold,
+  onFoldChange,
+}) => {
+  const { t } = useTranslation()
+
+  const handleFoldChange = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
+    e.stopPropagation()
+    onFoldChange(!isFold)
+  }, [isFold, onFoldChange])
+  return (
+    // text-[0px] to hide spacing between tooltip elements
+    <div className='shrink-0 cursor-pointer text-[0px]' onClick={handleFoldChange}>
+      <TooltipPlus
+        popupContent={t(`${I18N_PREFIX}.${isFold ? 'expand' : 'collapse'}`)}
+        hideArrow
+      >
+        {isFold && (
+          <div className='p-1 rounded-md text-gray-500 hover:text-gray-800 hover:bg-black/5'>
+            <ChevronDoubleDownIcon className='w-4 h-4' />
+          </div>
+        )}
+        {!isFold && (
+          <div className='p-2 rounded-lg text-gray-500 border-[0.5px] border-gray-200 hover:text-gray-800 hover:bg-black/5'>
+            <ChevronDoubleDownIcon className='w-4 h-4 transform rotate-180' />
+          </div>
+        )}
+      </TooltipPlus>
+    </div>
+  )
+}
+export default React.memo(ToggleFoldBtn)

+ 28 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx

@@ -0,0 +1,28 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import cn from 'classnames'
+import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing'
+
+type Props = {
+  className?: string
+  size: 'lg' | 'md'
+}
+
+const sizeClassMap = {
+  lg: 'w-9 h-9 p-2 rounded-[10px]',
+  md: 'w-6 h-6 p-1 rounded-lg',
+}
+
+const TracingIcon: FC<Props> = ({
+  className,
+  size,
+}) => {
+  const sizeClass = sizeClassMap[size]
+  return (
+    <div className={cn(className, sizeClass, 'bg-primary-500 shadow-md')}>
+      <Icon className='w-full h-full' />
+    </div>
+  )
+}
+export default React.memo(TracingIcon)

+ 16 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type.ts

@@ -0,0 +1,16 @@
+export enum TracingProvider {
+  langSmith = 'langsmith',
+  langfuse = 'langfuse',
+}
+
+export type LangSmithConfig = {
+  api_key: string
+  project: string
+  endpoint: string
+}
+
+export type LangFuseConfig = {
+  public_key: string
+  secret_key: string
+  host: string
+}

Файловите разлики са ограничени, защото са твърде много
+ 9 - 0
web/app/components/base/icons/assets/public/tracing/langfuse-icon-big.svg


Файловите разлики са ограничени, защото са твърде много
+ 9 - 0
web/app/components/base/icons/assets/public/tracing/langfuse-icon.svg


Файловите разлики са ограничени, защото са твърде много
+ 19 - 0
web/app/components/base/icons/assets/public/tracing/langsmith-icon-big.svg


Файловите разлики са ограничени, защото са твърде много
+ 19 - 0
web/app/components/base/icons/assets/public/tracing/langsmith-icon.svg


+ 6 - 0
web/app/components/base/icons/assets/public/tracing/tracing-icon.svg

@@ -0,0 +1,6 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="analytics-fill">
+<path id="Vector" opacity="0.6" d="M5 2.5C3.61929 2.5 2.5 3.61929 2.5 5V9.16667H6.15164C6.78293 9.16667 7.36003 9.52333 7.64235 10.088L8.33333 11.4699L10.9213 6.29399C11.0625 6.01167 11.351 5.83333 11.6667 5.83333C11.9823 5.83333 12.2708 6.01167 12.412 6.29399L13.8483 9.16667H17.5V5C17.5 3.61929 16.3807 2.5 15 2.5H5Z" fill="white"/>
+<path id="Vector_2" d="M2.5 14.9999C2.5 16.3807 3.61929 17.4999 5 17.4999H15C16.3807 17.4999 17.5 16.3807 17.5 14.9999V10.8333H13.8483C13.2171 10.8333 12.64 10.4766 12.3577 9.91195L11.6667 8.53003L9.07867 13.7059C8.9375 13.9883 8.649 14.1666 8.33333 14.1666C8.01769 14.1666 7.72913 13.9883 7.58798 13.7059L6.15164 10.8333H2.5V14.9999Z" fill="white"/>
+</g>
+</svg>

Файловите разлики са ограничени, защото са твърде много
+ 73 - 0
web/app/components/base/icons/src/public/tracing/LangfuseIcon.json


+ 16 - 0
web/app/components/base/icons/src/public/tracing/LangfuseIcon.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './LangfuseIcon.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 = 'LangfuseIcon'
+
+export default Icon

Файловите разлики са ограничени, защото са твърде много
+ 73 - 0
web/app/components/base/icons/src/public/tracing/LangfuseIconBig.json


+ 16 - 0
web/app/components/base/icons/src/public/tracing/LangfuseIconBig.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './LangfuseIconBig.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 = 'LangfuseIconBig'
+
+export default Icon

Файловите разлики са ограничени, защото са твърде много
+ 173 - 0
web/app/components/base/icons/src/public/tracing/LangsmithIcon.json


+ 16 - 0
web/app/components/base/icons/src/public/tracing/LangsmithIcon.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './LangsmithIcon.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 = 'LangsmithIcon'
+
+export default Icon

Файловите разлики са ограничени, защото са твърде много
+ 173 - 0
web/app/components/base/icons/src/public/tracing/LangsmithIconBig.json


+ 16 - 0
web/app/components/base/icons/src/public/tracing/LangsmithIconBig.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './LangsmithIconBig.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 = 'LangsmithIconBig'
+
+export default Icon

+ 47 - 0
web/app/components/base/icons/src/public/tracing/TracingIcon.json

@@ -0,0 +1,47 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "20",
+			"height": "20",
+			"viewBox": "0 0 20 20",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "analytics-fill"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector",
+							"opacity": "0.6",
+							"d": "M5 2.5C3.61929 2.5 2.5 3.61929 2.5 5V9.16667H6.15164C6.78293 9.16667 7.36003 9.52333 7.64235 10.088L8.33333 11.4699L10.9213 6.29399C11.0625 6.01167 11.351 5.83333 11.6667 5.83333C11.9823 5.83333 12.2708 6.01167 12.412 6.29399L13.8483 9.16667H17.5V5C17.5 3.61929 16.3807 2.5 15 2.5H5Z",
+							"fill": "white"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Vector_2",
+							"d": "M2.5 14.9999C2.5 16.3807 3.61929 17.4999 5 17.4999H15C16.3807 17.4999 17.5 16.3807 17.5 14.9999V10.8333H13.8483C13.2171 10.8333 12.64 10.4766 12.3577 9.91195L11.6667 8.53003L9.07867 13.7059C8.9375 13.9883 8.649 14.1666 8.33333 14.1666C8.01769 14.1666 7.72913 13.9883 7.58798 13.7059L6.15164 10.8333H2.5V14.9999Z",
+							"fill": "white"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "TracingIcon"
+}

+ 16 - 0
web/app/components/base/icons/src/public/tracing/TracingIcon.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './TracingIcon.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 = 'TracingIcon'
+
+export default Icon

+ 5 - 0
web/app/components/base/icons/src/public/tracing/index.ts

@@ -0,0 +1,5 @@
+export { default as LangfuseIconBig } from './LangfuseIconBig'
+export { default as LangfuseIcon } from './LangfuseIcon'
+export { default as LangsmithIconBig } from './LangsmithIconBig'
+export { default as LangsmithIcon } from './LangsmithIcon'
+export { default as TracingIcon } from './TracingIcon'

+ 4 - 0
web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx

@@ -160,6 +160,10 @@ const CodeEditor: FC<Props> = ({
           // lineNumbers: (num) => {
           //   return <div>{num}</div>
           // }
+          // hide ambiguousCharacters warning
+          unicodeHighlight: {
+            ambiguousCharacters: false,
+          },
         }}
         onMount={handleEditorDidMount}
       />

+ 36 - 0
web/i18n/en-US/app.ts

@@ -85,6 +85,42 @@ const translation = {
     workflow: 'Workflow',
     completion: 'Completion',
   },
+  tracing: {
+    title: 'Tracing app performance',
+    description: 'Configuring a Third-Party LLMOps provider and tracing app performance.',
+    config: 'Config',
+    collapse: 'Collapse',
+    expand: 'Expand',
+    tracing: 'Tracing',
+    disabled: 'Disabled',
+    disabledTip: 'Please config provider first',
+    enabled: 'In Service',
+    tracingDescription: 'Capture the full context of app execution, including LLM calls, context, prompts, HTTP requests, and more, to a third-party tracing platform.',
+    configProviderTitle: {
+      configured: 'Configured',
+      notConfigured: 'Config provider to enable tracing',
+      moreProvider: 'More Provider',
+    },
+    langsmith: {
+      title: 'LangSmith',
+      description: 'An all-in-one developer platform for every step of the LLM-powered application lifecycle.',
+    },
+    langfuse: {
+      title: 'Langfuse',
+      description: 'Traces, evals, prompt management and metrics to debug and improve your LLM application.',
+    },
+    inUse: 'In use',
+    configProvider: {
+      title: 'Config ',
+      placeholder: 'Enter your {{key}}',
+      project: 'Project',
+      publicKey: 'Public Key',
+      secretKey: 'Secret Key',
+      viewDocsLink: 'View {{key}} docs',
+      removeConfirmTitle: 'Remove {{key}} configuration?',
+      removeConfirmContent: 'The current configuration is in use, removing it will turn off the Tracing feature.',
+    },
+  },
 }
 
 export default translation

+ 2 - 1
web/i18n/en-US/common.ts

@@ -12,6 +12,7 @@ const translation = {
     cancel: 'Cancel',
     clear: 'Clear',
     save: 'Save',
+    saveAndEnable: 'Save & Enable',
     edit: 'Edit',
     add: 'Add',
     added: 'Added',
@@ -439,7 +440,7 @@ const translation = {
     latestAvailable: 'Dify {{version}} is the latest version available.',
   },
   appMenus: {
-    overview: 'Overview',
+    overview: 'Monitoring',
     promptEng: 'Orchestrate',
     apiAccess: 'API Access',
     logAndAnn: 'Logs & Ann.',

+ 36 - 0
web/i18n/zh-Hans/app.ts

@@ -84,6 +84,42 @@ const translation = {
     workflow: '工作流',
     completion: '文本生成',
   },
+  tracing: {
+    title: '追踪应用性能',
+    description: '配置第三方 LLMOps 提供商并跟踪应用程序性能。',
+    config: '配置',
+    collapse: '折叠',
+    expand: '展开',
+    tracing: '追踪',
+    disabled: '已禁用',
+    disabledTip: '请先配置提供商',
+    enabled: '已启用',
+    tracingDescription: '捕获应用程序执行的完整上下文,包括 LLM 调用、上下文、提示、HTTP 请求等,发送到第三方跟踪平台。',
+    configProviderTitle: {
+      configured: '已配置',
+      notConfigured: '配置提供商以启用追踪',
+      moreProvider: '更多提供商',
+    },
+    langsmith: {
+      title: 'LangSmith',
+      description: '一个全方位的开发者平台,适用于 LLM 驱动应用程序生命周期的每个步骤。',
+    },
+    langfuse: {
+      title: 'Langfuse',
+      description: '跟踪、评估、提示管理和指标,以调试和改进您的 LLM 应用程序。',
+    },
+    inUse: '使用中',
+    configProvider: {
+      title: '配置 ',
+      placeholder: '输入你的{{key}}',
+      project: '项目',
+      publicKey: '公钥',
+      secretKey: '密钥',
+      viewDocsLink: '查看 {{key}} 的文档',
+      removeConfirmTitle: '删除 {{key}} 配置?',
+      removeConfirmContent: '当前配置正在使用中,删除它将关闭追踪功能。',
+    },
+  },
 }
 
 export default translation

+ 2 - 1
web/i18n/zh-Hans/common.ts

@@ -12,6 +12,7 @@ const translation = {
     cancel: '取消',
     clear: '清空',
     save: '保存',
+    saveAndEnable: '保存并启用',
     edit: '编辑',
     add: '添加',
     added: '已添加',
@@ -435,7 +436,7 @@ const translation = {
     latestAvailable: 'Dify {{version}} 已是最新版本。',
   },
   appMenus: {
-    overview: '概览',
+    overview: '监测',
     promptEng: '编排',
     apiAccess: '访问 API',
     logAndAnn: '日志与标注',

+ 11 - 0
web/models/app.ts

@@ -1,3 +1,4 @@
+import type { LangFuseConfig, LangSmithConfig, TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
 import type { App, AppTemplate, SiteConfig } from '@/types/app'
 
 /* export type App = {
@@ -129,3 +130,13 @@ export type AppVoicesListResponse = [{
   name: string
   value: string
 }]
+
+export type TracingStatus = {
+  enabled: boolean
+  tracing_provider: TracingProvider | null
+}
+
+export type TracingConfig = {
+  tracing_provider: TracingProvider
+  tracing_config: LangSmithConfig | LangFuseConfig
+}

+ 32 - 2
web/service/apps.ts

@@ -1,8 +1,9 @@
 import type { Fetcher } from 'swr'
-import { del, get, post, put } from './base'
-import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
+import { del, get, patch, post, put } from './base'
+import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
 import type { CommonResponse } from '@/models/common'
 import type { AppMode, ModelConfig } from '@/types/app'
+import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
 
 export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => {
   return get<AppListResponse>(url, { params })
@@ -121,3 +122,32 @@ export const generationIntroduction: Fetcher<GenerationIntroductionResponse, { u
 export const fetchAppVoices: Fetcher<AppVoicesListResponse, { appId: string; language?: string }> = ({ appId, language }) => {
   return get<AppVoicesListResponse>(`apps/${appId}/text-to-audio/voices?language=${language}`)
 }
+
+// Tracing
+export const fetchTracingStatus: Fetcher<TracingStatus, { appId: string }> = ({ appId }) => {
+  return get(`/apps/${appId}/trace`)
+}
+
+export const updateTracingStatus: Fetcher<CommonResponse, { appId: string; body: Record<string, any> }> = ({ appId, body }) => {
+  return post(`/apps/${appId}/trace`, { body })
+}
+
+export const fetchTracingConfig: Fetcher<TracingConfig & { has_not_configured: true }, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
+  return get(`/apps/${appId}/trace-config`, {
+    params: {
+      tracing_provider: provider,
+    },
+  })
+}
+
+export const addTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
+  return post(`/apps/${appId}/trace-config`, { body })
+}
+
+export const updateTracingConfig: Fetcher<CommonResponse, { appId: string; body: TracingConfig }> = ({ appId, body }) => {
+  return patch(`/apps/${appId}/trace-config`, { body })
+}
+
+export const removeTracingConfig: Fetcher<CommonResponse, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
+  return del(`/apps/${appId}/trace-config?tracing_provider=${provider}`)
+}

Някои файлове не бяха показани, защото твърде много файлове са промени