| @@ -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 | |||
| } | |||
| @@ -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> | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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" | |||
| } | |||
| @@ -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 | |||
| @@ -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' | |||
| @@ -160,6 +160,10 @@ const CodeEditor: FC<Props> = ({ | |||
| // lineNumbers: (num) => { | |||
| // return <div>{num}</div> | |||
| // } | |||
| // hide ambiguousCharacters warning | |||
| unicodeHighlight: { | |||
| ambiguousCharacters: false, | |||
| }, | |||
| }} | |||
| onMount={handleEditorDidMount} | |||
| /> | |||
| @@ -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 | |||
| @@ -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.', | |||
| @@ -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 | |||
| @@ -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: '日志与标注', | |||
| @@ -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 | |||
| } | |||
| @@ -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}`) | |||
| } | |||