mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 03:16:51 +08:00
feat: tracing fe (#5487)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { TracingProvider } from './type'
|
||||
|
||||
export const docURL = {
|
||||
[TracingProvider.langSmith]: 'https://docs.smith.langchain.com/',
|
||||
[TracingProvider.langfuse]: 'https://docs.langfuse.com',
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user