Feat: Add LangfuseCard component. #6155 (#6468)

### What problem does this PR solve?

Feat: Add LangfuseCard component. #6155

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-03-24 19:07:55 +08:00 committed by GitHub
parent 5e0a77df2b
commit 3c57a9986c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 431 additions and 8 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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 };
};

View File

@ -0,0 +1,7 @@
export interface ILangfuseConfig {
secret_key: string;
public_key: string;
host: string;
project_id: string;
project_name: string;
}

View File

@ -0,0 +1,5 @@
export interface ISetLangfuseConfigRequestBody {
secret_key: string;
public_key: string;
host: string;
}

View File

@ -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!',

View File

@ -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: '註冊成功',

View File

@ -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: '注册成功',

View File

@ -1,7 +1,3 @@
.modelWrapper {
width: 100%;
}
.modelContainer {
width: 100%;
.factoryOperationWrapper {

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
};
};

View File

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

View File

@ -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`,