### What problem does this PR solve? Feat: Add LangfuseCard component. #6155 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.18.0
| @@ -2,12 +2,14 @@ import { LanguageTranslationMap } from '@/constants/common'; | |||
| import { ResponseGetType } from '@/interfaces/database/base'; | |||
| import { IToken } from '@/interfaces/database/chat'; | |||
| import { ITenantInfo } from '@/interfaces/database/knowledge'; | |||
| import { ILangfuseConfig } from '@/interfaces/database/system'; | |||
| import { | |||
| ISystemStatus, | |||
| ITenant, | |||
| ITenantUser, | |||
| IUserInfo, | |||
| } from '@/interfaces/database/user-setting'; | |||
| import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system'; | |||
| import userService, { | |||
| addTenantUser, | |||
| agreeTenant, | |||
| @@ -375,3 +377,57 @@ export const useAgreeTenant = () => { | |||
| return { data, loading, agreeTenant: mutateAsync }; | |||
| }; | |||
| export const useSetLangfuseConfig = () => { | |||
| const { t } = useTranslation(); | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: ['setLangfuseConfig'], | |||
| mutationFn: async (params: ISetLangfuseConfigRequestBody) => { | |||
| const { data } = await userService.setLangfuseConfig(params); | |||
| if (data.code === 0) { | |||
| message.success(t('message.operated')); | |||
| } | |||
| return data?.code; | |||
| }, | |||
| }); | |||
| return { data, loading, setLangfuseConfig: mutateAsync }; | |||
| }; | |||
| export const useDeleteLangfuseConfig = () => { | |||
| const { t } = useTranslation(); | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: ['deleteLangfuseConfig'], | |||
| mutationFn: async () => { | |||
| const { data } = await userService.deleteLangfuseConfig(); | |||
| if (data.code === 0) { | |||
| message.success(t('message.deleted')); | |||
| } | |||
| return data?.code; | |||
| }, | |||
| }); | |||
| return { data, loading, deleteLangfuseConfig: mutateAsync }; | |||
| }; | |||
| export const useFetchLangfuseConfig = () => { | |||
| const { data, isFetching: loading } = useQuery<ILangfuseConfig>({ | |||
| queryKey: ['fetchLangfuseConfig'], | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await userService.getLangfuseConfig(); | |||
| return data?.data; | |||
| }, | |||
| }); | |||
| return { data, loading }; | |||
| }; | |||
| @@ -0,0 +1,7 @@ | |||
| export interface ILangfuseConfig { | |||
| secret_key: string; | |||
| public_key: string; | |||
| host: string; | |||
| project_id: string; | |||
| project_name: string; | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| export interface ISetLangfuseConfigRequestBody { | |||
| secret_key: string; | |||
| public_key: string; | |||
| host: string; | |||
| } | |||
| @@ -699,6 +699,16 @@ This auto-tag feature enhances retrieval by adding another layer of domain-speci | |||
| sureDelete: 'Are you sure to remove this member?', | |||
| quit: 'Quit', | |||
| sureQuit: 'Are you sure you want to quit the team you joined?', | |||
| secretKey: 'Secret key', | |||
| publicKey: 'Public key', | |||
| secretKeyMessage: 'Please enter the secret key', | |||
| publicKeyMessage: 'Please enter the public key', | |||
| hostMessage: 'Please enter the host', | |||
| configuration: 'Configuration', | |||
| langfuseDescription: | |||
| 'Traces, evals, prompt management and metrics to debug and improve your LLM application.', | |||
| viewLangfuseSDocumentation: "View Langfuse's documentation", | |||
| view: 'View', | |||
| }, | |||
| message: { | |||
| registered: 'Registered!', | |||
| @@ -211,7 +211,8 @@ export default { | |||
| embeddingModelTip: | |||
| '用於嵌入塊的嵌入模型。一旦知識庫有了塊,它就無法更改。如果你想改變它,你需要刪除所有的塊。', | |||
| permissionsTip: '如果權限是“團隊”,則所有團隊成員都可以操作知識庫。', | |||
| chunkTokenNumberTip: '建議的生成文本塊的 token 數閾值。如果切分得到的小文本段 token 數達不到這一閾值,系統就會不斷與之後的文本段合併,直至再合併下一個文本段會超過這一閾值為止,此時產生一個最終文本塊。如果系統在切分文本段時始終沒有遇到文本分段標識符,即便文本段 token 數已經超過這一閾值,系統也不會生成新文本塊。', | |||
| chunkTokenNumberTip: | |||
| '建議的生成文本塊的 token 數閾值。如果切分得到的小文本段 token 數達不到這一閾值,系統就會不斷與之後的文本段合併,直至再合併下一個文本段會超過這一閾值為止,此時產生一個最終文本塊。如果系統在切分文本段時始終沒有遇到文本分段標識符,即便文本段 token 數已經超過這一閾值,系統也不會生成新文本塊。', | |||
| chunkMethod: '切片方法', | |||
| chunkMethodTip: '說明位於右側。', | |||
| upload: '上傳', | |||
| @@ -668,6 +669,16 @@ export default { | |||
| sureDelete: '您確定刪除該成員嗎?', | |||
| quit: '退出', | |||
| sureQuit: '確定退出加入的團隊嗎?', | |||
| secretKey: '密鑰', | |||
| publicKey: '公鑰', | |||
| secretKeyMessage: '請輸入私钥', | |||
| publicKeyMessage: '請輸入公钥', | |||
| hostMessage: '請輸入 host', | |||
| configuration: '配置', | |||
| langfuseDescription: | |||
| '追蹤、評估、提示管理和指標以調試和改進您的 LLM 應用程式。', | |||
| viewLangfuseSDocumentation: '查看 Langfuse 的文檔', | |||
| view: '查看', | |||
| }, | |||
| message: { | |||
| registered: '註冊成功', | |||
| @@ -210,8 +210,10 @@ export default { | |||
| chunkTokenNumberMessage: '块Token数是必填项', | |||
| embeddingModelTip: | |||
| '用于嵌入块的嵌入模型。 一旦知识库有了块,它就无法更改。 如果你想改变它,你需要删除所有的块。', | |||
| permissionsTip: '如果把知识库权限设为“团队”,则所有团队成员都可以操作该知识库。', | |||
| chunkTokenNumberTip: '建议的生成文本块的 token 数阈值。如果切分得到的小文本段 token 数达不到这一阈值就会不断与之后的文本段合并,直至再合并下一个文本段会超过这一阈值为止,此时产生一个最终文本块。如果系统在切分文本段时始终没有遇到文本分段标识符,即便文本段 token 数已经超过这一阈值,系统也不会生成新文本块。', | |||
| permissionsTip: | |||
| '如果把知识库权限设为“团队”,则所有团队成员都可以操作该知识库。', | |||
| chunkTokenNumberTip: | |||
| '建议的生成文本块的 token 数阈值。如果切分得到的小文本段 token 数达不到这一阈值就会不断与之后的文本段合并,直至再合并下一个文本段会超过这一阈值为止,此时产生一个最终文本块。如果系统在切分文本段时始终没有遇到文本分段标识符,即便文本段 token 数已经超过这一阈值,系统也不会生成新文本块。', | |||
| chunkMethod: '切片方法', | |||
| chunkMethodTip: '说明位于右侧。', | |||
| upload: '上传', | |||
| @@ -687,6 +689,16 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 | |||
| sureDelete: '您确定要删除该成员吗?', | |||
| quit: '退出', | |||
| sureQuit: '确定退出加入的团队吗?', | |||
| secretKey: '密钥', | |||
| publicKey: '公钥', | |||
| secretKeyMessage: '请输入私钥', | |||
| publicKeyMessage: '请输入公钥', | |||
| hostMessage: '请输入 host', | |||
| configuration: '配置', | |||
| langfuseDescription: | |||
| '跟踪、评估、提示管理和指标,以调试和改进您的 LLM 应用程序。', | |||
| viewLangfuseSDocumentation: '查看 Langfuse 的文档', | |||
| view: '查看', | |||
| }, | |||
| message: { | |||
| registered: '注册成功', | |||
| @@ -1,7 +1,3 @@ | |||
| .modelWrapper { | |||
| width: 100%; | |||
| } | |||
| .modelContainer { | |||
| width: 100%; | |||
| .factoryOperationWrapper { | |||
| @@ -49,6 +49,7 @@ import { | |||
| } from './hooks'; | |||
| import HunyuanModal from './hunyuan-modal'; | |||
| import styles from './index.less'; | |||
| import { LangfuseCard } from './langfuse'; | |||
| import OllamaModal from './ollama-modal'; | |||
| import SparkModal from './spark-modal'; | |||
| import SystemModelSettingModal from './system-model-setting-modal'; | |||
| @@ -358,7 +359,8 @@ const UserSettingModel = () => { | |||
| ]; | |||
| return ( | |||
| <section id="xx" className={styles.modelWrapper}> | |||
| <section id="xx" className="w-full space-y-6"> | |||
| <LangfuseCard></LangfuseCard> | |||
| <Spin spinning={loading}> | |||
| <section className={styles.modelContainer}> | |||
| <SettingTitle | |||
| @@ -0,0 +1,69 @@ | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| Card, | |||
| CardDescription, | |||
| CardHeader, | |||
| CardTitle, | |||
| } from '@/components/ui/card'; | |||
| import { useFetchLangfuseConfig } from '@/hooks/user-setting-hooks'; | |||
| import { Eye, Settings2 } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { LangfuseConfigurationDialog } from './langfuse-configuration-dialog'; | |||
| import { useSaveLangfuseConfiguration } from './use-save-langfuse-configuration'; | |||
| export function LangfuseCard() { | |||
| const { | |||
| saveLangfuseConfigurationOk, | |||
| showSaveLangfuseConfigurationModal, | |||
| hideSaveLangfuseConfigurationModal, | |||
| saveLangfuseConfigurationVisible, | |||
| loading, | |||
| } = useSaveLangfuseConfiguration(); | |||
| const { t } = useTranslation(); | |||
| const { data } = useFetchLangfuseConfig(); | |||
| const handleView = useCallback(() => { | |||
| window.open( | |||
| `https://cloud.langfuse.com/project/${data?.project_id}`, | |||
| '_blank', | |||
| ); | |||
| }, [data?.project_id]); | |||
| return ( | |||
| <Card> | |||
| <CardHeader> | |||
| <CardTitle className="flex justify-between"> | |||
| <div className="flex items-center gap-4"> | |||
| <SvgIcon name={'langfuse'} width={24} height={24}></SvgIcon> | |||
| Langfuse | |||
| </div> | |||
| <div className="flex gap-4 items-center"> | |||
| {data && ( | |||
| <Button variant={'outline'} size={'sm'} onClick={handleView}> | |||
| <Eye /> {t('setting.view')} | |||
| </Button> | |||
| )} | |||
| <Button | |||
| size={'sm'} | |||
| onClick={showSaveLangfuseConfigurationModal} | |||
| className="bg-blue-500 hover:bg-blue-400" | |||
| > | |||
| <Settings2 /> | |||
| {t('setting.configuration')} | |||
| </Button> | |||
| </div> | |||
| </CardTitle> | |||
| <CardDescription>{t('setting.langfuseDescription')}</CardDescription> | |||
| </CardHeader> | |||
| {saveLangfuseConfigurationVisible && ( | |||
| <LangfuseConfigurationDialog | |||
| hideModal={hideSaveLangfuseConfigurationModal} | |||
| onOk={saveLangfuseConfigurationOk} | |||
| loading={loading} | |||
| ></LangfuseConfigurationDialog> | |||
| )} | |||
| </Card> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { | |||
| Dialog, | |||
| DialogContent, | |||
| DialogFooter, | |||
| DialogHeader, | |||
| DialogTitle, | |||
| DialogTrigger, | |||
| } from '@/components/ui/dialog'; | |||
| import { LoadingButton } from '@/components/ui/loading-button'; | |||
| import { useDeleteLangfuseConfig } from '@/hooks/user-setting-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { ExternalLink, Trash2 } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { | |||
| FormId, | |||
| LangfuseConfigurationForm, | |||
| } from './langfuse-configuration-form'; | |||
| export function LangfuseConfigurationDialog({ | |||
| hideModal, | |||
| loading, | |||
| onOk, | |||
| }: IModalProps<any>) { | |||
| const { t } = useTranslation(); | |||
| const { deleteLangfuseConfig } = useDeleteLangfuseConfig(); | |||
| const handleDelete = useCallback(async () => { | |||
| const ret = await deleteLangfuseConfig(); | |||
| if (ret === 0) { | |||
| hideModal?.(); | |||
| } | |||
| }, [deleteLangfuseConfig, hideModal]); | |||
| return ( | |||
| <Dialog open onOpenChange={hideModal}> | |||
| <DialogTrigger asChild> | |||
| <Button variant="outline"></Button> | |||
| </DialogTrigger> | |||
| <DialogContent> | |||
| <DialogHeader> | |||
| <DialogTitle>{t('setting.configuration')} Langfuse</DialogTitle> | |||
| </DialogHeader> | |||
| <LangfuseConfigurationForm onOk={onOk}></LangfuseConfigurationForm> | |||
| <DialogFooter className="!justify-between"> | |||
| <a | |||
| href="https://langfuse.com/docs" | |||
| className="flex items-center gap-2 underline text-blue-600 hover:text-blue-800 visited:text-purple-600" | |||
| target="_blank" | |||
| rel="noreferrer" | |||
| > | |||
| {t('setting.viewLangfuseSDocumentation')} | |||
| <ExternalLink className="size-4" /> | |||
| </a> | |||
| <div className="flex items-center gap-4"> | |||
| <ConfirmDeleteDialog onOk={handleDelete}> | |||
| <Button variant={'outline'}> | |||
| <Trash2 className="text-red-500" /> {t('common.delete')} | |||
| </Button> | |||
| </ConfirmDeleteDialog> | |||
| <LoadingButton type="submit" form={FormId} loading={loading}> | |||
| {t('common.save')} | |||
| </LoadingButton> | |||
| </div> | |||
| </DialogFooter> | |||
| </DialogContent> | |||
| </Dialog> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,126 @@ | |||
| 'use client'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { useFetchLangfuseConfig } from '@/hooks/user-setting-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { useEffect } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| export const FormId = 'LangfuseConfigurationForm'; | |||
| export function LangfuseConfigurationForm({ onOk }: IModalProps<any>) { | |||
| const { t } = useTranslation(); | |||
| const { data } = useFetchLangfuseConfig(); | |||
| const FormSchema = z.object({ | |||
| secret_key: z | |||
| .string() | |||
| .min(1, { | |||
| message: t('setting.secretKeyMessage'), | |||
| }) | |||
| .trim(), | |||
| public_key: z | |||
| .string() | |||
| .min(1, { | |||
| message: t('setting.publicKeyMessage'), | |||
| }) | |||
| .trim(), | |||
| host: z | |||
| .string() | |||
| .min(0, { | |||
| message: t('setting.hostMessage'), | |||
| }) | |||
| .trim(), | |||
| }); | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| resolver: zodResolver(FormSchema), | |||
| defaultValues: {}, | |||
| }); | |||
| async function onSubmit(data: z.infer<typeof FormSchema>) { | |||
| onOk?.(data); | |||
| } | |||
| useEffect(() => { | |||
| if (data) { | |||
| form.reset(data); | |||
| } | |||
| }, [data, form]); | |||
| return ( | |||
| <Form {...form}> | |||
| <form | |||
| onSubmit={form.handleSubmit(onSubmit)} | |||
| className="space-y-6" | |||
| id={FormId} | |||
| > | |||
| <FormField | |||
| control={form.control} | |||
| name="secret_key" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('setting.secretKey')}</FormLabel> | |||
| <FormControl> | |||
| <Input | |||
| type={'password'} | |||
| placeholder={t('setting.secretKeyMessage')} | |||
| {...field} | |||
| autoComplete="off" | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="public_key" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('setting.publicKey')}</FormLabel> | |||
| <FormControl> | |||
| <Input | |||
| type={'password'} | |||
| placeholder={t('setting.publicKeyMessage')} | |||
| {...field} | |||
| autoComplete="off" | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="host" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>Host</FormLabel> | |||
| <FormControl> | |||
| <Input | |||
| placeholder={'https://cloud.langfuse.com'} | |||
| {...field} | |||
| autoComplete="off" | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </form> | |||
| </Form> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,33 @@ | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { useSetLangfuseConfig } from '@/hooks/user-setting-hooks'; | |||
| import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system'; | |||
| import { useCallback } from 'react'; | |||
| export const useSaveLangfuseConfiguration = () => { | |||
| const { | |||
| visible: saveLangfuseConfigurationVisible, | |||
| hideModal: hideSaveLangfuseConfigurationModal, | |||
| showModal: showSaveLangfuseConfigurationModal, | |||
| } = useSetModalState(); | |||
| const { setLangfuseConfig, loading } = useSetLangfuseConfig(); | |||
| const onSaveLangfuseConfigurationOk = useCallback( | |||
| async (params: ISetLangfuseConfigRequestBody) => { | |||
| const ret = await setLangfuseConfig(params); | |||
| if (ret === 0) { | |||
| hideSaveLangfuseConfigurationModal(); | |||
| } | |||
| return ret; | |||
| }, | |||
| [hideSaveLangfuseConfigurationModal], | |||
| ); | |||
| return { | |||
| loading, | |||
| saveLangfuseConfigurationOk: onSaveLangfuseConfigurationOk, | |||
| saveLangfuseConfigurationVisible, | |||
| hideSaveLangfuseConfigurationModal, | |||
| showSaveLangfuseConfigurationModal, | |||
| }; | |||
| }; | |||
| @@ -23,6 +23,7 @@ const { | |||
| removeSystemToken, | |||
| createSystemToken, | |||
| getSystemConfig, | |||
| setLangfuseConfig, | |||
| } = api; | |||
| const methods = { | |||
| @@ -106,6 +107,18 @@ const methods = { | |||
| url: getSystemConfig, | |||
| method: 'get', | |||
| }, | |||
| setLangfuseConfig: { | |||
| url: setLangfuseConfig, | |||
| method: 'put', | |||
| }, | |||
| getLangfuseConfig: { | |||
| url: setLangfuseConfig, | |||
| method: 'get', | |||
| }, | |||
| deleteLangfuseConfig: { | |||
| url: setLangfuseConfig, | |||
| method: 'delete', | |||
| }, | |||
| } as const; | |||
| const userService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -120,6 +120,7 @@ export default { | |||
| listSystemToken: `${api_host}/system/token_list`, | |||
| removeSystemToken: `${api_host}/system/token`, | |||
| getSystemConfig: `${api_host}/system/config`, | |||
| setLangfuseConfig: `${api_host}/langfuse/api_key`, | |||
| // flow | |||
| listTemplates: `${api_host}/canvas/templates`, | |||