|  | @@ -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)
 |