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
15 changed files with 431 additions and 8 deletions

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