feat: rag fe framework

This commit is contained in:
Junyan Qin
2025-07-06 15:52:53 +08:00
parent bef0d73e83
commit ebd8e014c6
10 changed files with 398 additions and 59 deletions

View File

@@ -130,7 +130,6 @@ export default function BotDetailDialog({
onFormCancel={handleFormCancel}
onBotDeleted={handleBotDeleted}
onNewBotCreated={handleNewBotCreated}
hideButtons={true}
/>
</div>
<DialogFooter className="px-6 py-4 border-t shrink-0">
@@ -202,7 +201,6 @@ export default function BotDetailDialog({
onFormCancel={handleFormCancel}
onBotDeleted={handleBotDeleted}
onNewBotCreated={handleNewBotCreated}
hideButtons={true}
/>
)}
{activeMenu === 'logs' && botId && (

View File

@@ -67,14 +67,12 @@ export default function BotForm({
onFormCancel,
onBotDeleted,
onNewBotCreated,
hideButtons = false,
}: {
initBotId?: string;
onFormSubmit: (value: z.infer<ReturnType<typeof getFormSchema>>) => void;
onFormCancel: () => void;
onBotDeleted: () => void;
onNewBotCreated: (botId: string) => void;
hideButtons?: boolean;
}) {
const { t } = useTranslation();
const formSchema = getFormSchema(t);
@@ -527,45 +525,6 @@ export default function BotForm({
</div>
)}
</div>
{!hideButtons && (
<div className="sticky bottom-0 left-0 right-0 bg-background border-t p-4 mt-4">
<div className="flex justify-end gap-2">
{!initBotId && (
<Button
type="submit"
onClick={form.handleSubmit(onDynamicFormSubmit)}
>
{t('common.submit')}
</Button>
)}
{initBotId && (
<>
<Button
type="button"
variant="destructive"
onClick={() => setShowDeleteConfirmModal(true)}
>
{t('common.delete')}
</Button>
<Button
type="button"
onClick={form.handleSubmit(onDynamicFormSubmit)}
>
{t('common.save')}
</Button>
</>
)}
<Button
type="button"
variant="outline"
onClick={() => onFormCancel()}
>
{t('common.cancel')}
</Button>
</div>
</div>
)}
</form>
</Form>
</div>

View File

@@ -92,7 +92,7 @@ export default function BotConfigPage() {
}
return (
<div className={styles.configPageContainer}>
<div>
<BotDetailDialog
open={detailDialogOpen}
onOpenChange={setDetailDialogOpen}

View File

@@ -8,16 +8,28 @@ import {
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
} from '@/components/ui/sidebar';
import { Button } from '@/components/ui/button';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { httpClient } from '@/app/infra/http/HttpClient';
import { KnowledgeBase } from '@/app/infra/entities/api';
// import { httpClient } from '@/app/infra/http/HttpClient';
// import { KnowledgeBase } from '@/app/infra/entities/api';
import KBForm from '@/app/home/knowledge/components/kb-form/KBForm';
interface KBDetailDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
kbId?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onFormSubmit: (value: z.infer<any>) => void;
onFormCancel: () => void;
onKbDeleted: () => void;
@@ -36,7 +48,7 @@ export default function KBDetailDialog({
const { t } = useTranslation();
const [kbId, setKbId] = useState<string | undefined>(propKbId);
const [activeMenu, setActiveMenu] = useState('metadata');
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
// const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
useEffect(() => {
setKbId(propKbId);
@@ -58,8 +70,8 @@ export default function KBDetailDialog({
),
},
{
key: 'files',
label: t('knowledge.files'),
key: 'documents',
label: t('knowledge.documents'),
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -79,11 +91,121 @@ export default function KBDetailDialog({
<DialogContent className="overflow-hidden p-0 !max-w-[40vw] max-h-[70vh] flex">
<main className="flex flex-1 flex-col h-[70vh]">
<DialogHeader className="px-6 pt-6 pb-4 shrink-0">
<DialogTitle>{t('knowledge.newKb')}</DialogTitle>
<DialogTitle>{t('knowledge.createKnowledgeBase')}</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 pb-6">
{activeMenu === 'metadata' && (
<KBForm
initKbId={undefined}
onFormSubmit={onFormSubmit}
onFormCancel={onFormCancel}
onKbDeleted={onKbDeleted}
onNewKbCreated={onNewKbCreated}
/>
)}
{activeMenu === 'documents' && <div>documents</div>}
</div>
{activeMenu === 'metadata' && (
<DialogFooter className="px-6 py-4 border-t shrink-0">
<div className="flex justify-end gap-2">
<Button type="submit" form="kb-form">
{t('common.save')}
</Button>
<Button
type="button"
variant="outline"
onClick={onFormCancel}
>
{t('common.cancel')}
</Button>
</div>
</DialogFooter>
)}
</main>
</DialogContent>
</Dialog>
);
}
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="overflow-hidden p-0 !max-w-[50rem] max-h-[75vh] flex">
<SidebarProvider className="items-start w-full flex">
<Sidebar
collapsible="none"
className="hidden md:flex h-[80vh] w-40 min-w-[120px] border-r bg-white"
>
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{menu.map((item) => (
<SidebarMenuItem key={item.key}>
<SidebarMenuButton
asChild
isActive={activeMenu === item.key}
onClick={() => setActiveMenu(item.key)}
>
<a href="#">
{item.icon}
<span>{item.label}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>
<main className="flex flex-1 flex-col h-[75vh]">
<DialogHeader className="px-6 pt-6 pb-4 shrink-0">
<DialogTitle>
{activeMenu === 'metadata'
? t('knowledge.createKnowledgeBase')
: t('knowledge.editDocument')}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 pb-6">
{activeMenu === 'metadata' && (
<KBForm
initKbId={kbId}
onFormSubmit={onFormSubmit}
onFormCancel={onFormCancel}
onKbDeleted={onKbDeleted}
onNewKbCreated={onNewKbCreated}
/>
)}
{activeMenu === 'documents' && <div>documents</div>}
</div>
{activeMenu === 'metadata' && (
<DialogFooter className="px-6 py-4 border-t shrink-0">
<div className="flex justify-end gap-2">
<Button
type="button"
variant="destructive"
onClick={onKbDeleted}
>
{t('common.delete')}
</Button>
<Button type="submit" form="kb-form">
{t('common.save')}
</Button>
<Button
type="button"
variant="outline"
onClick={onFormCancel}
>
{t('common.cancel')}
</Button>
</div>
</DialogFooter>
)}
</main>
</SidebarProvider>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -26,7 +26,7 @@ export default function KBCard({ kbCardVO }: { kbCardVO: KnowledgeBaseVO }) {
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path>
</svg>
<div className={`${styles.basicInfoUpdateTimeText}`}>
{t('knowledge.bases.updateTime')}
{t('knowledge.updateTime')}
{kbCardVO.lastUpdatedTimeAgo}
</div>
</div>

View File

@@ -0,0 +1,4 @@
export interface IEmbeddingModelEntity {
label: string;
value: string;
}

View File

@@ -0,0 +1,172 @@
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from '@/components/ui/form';
import { IEmbeddingModelEntity } from './ChooseEntity';
import { httpClient } from '@/app/infra/http/HttpClient';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
const getFormSchema = (t: (key: string) => string) =>
z.object({
name: z.string().min(1, { message: t('knowledge.kbNameRequired') }),
description: z
.string()
.min(1, { message: t('knowledge.kbDescriptionRequired') }),
embeddingModelUUID: z
.string()
.min(1, { message: t('knowledge.embeddingModelUUIDRequired') }),
});
export default function KBForm({
initKbId,
onFormSubmit,
onFormCancel,
onKbDeleted,
onNewKbCreated,
}: {
initKbId?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onFormSubmit: (value: any) => void;
onFormCancel: () => void;
onKbDeleted: () => void;
onNewKbCreated: (kbId: string) => void;
}) {
const { t } = useTranslation();
const formSchema = getFormSchema(t);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
description: t('knowledge.defaultDescription'),
embeddingModelUUID: '',
},
});
const [embeddingModelNameList, setEmbeddingModelNameList] = useState<
IEmbeddingModelEntity[]
>([]);
useEffect(() => {
getEmbeddingModelNameList();
}, []);
const getEmbeddingModelNameList = async () => {
const resp = await httpClient.getProviderEmbeddingModels();
setEmbeddingModelNameList(
resp.models.map((item) => {
return {
label: item.name,
value: item.uuid,
};
}),
);
};
return (
<>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onFormSubmit)}
id="kb-form"
className="space-y-8"
>
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('knowledge.kbName')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('knowledge.kbDescription')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="embeddingModelUUID"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('knowledge.embeddingModelUUID')}
<span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<div className="relative">
<Select
onValueChange={(value) => {
field.onChange(value);
console.log('value', value);
}}
value={field.value}
>
<SelectTrigger className="w-[180px]">
<SelectValue
placeholder={t('knowledge.selectEmbeddingModel')}
/>
</SelectTrigger>
<SelectContent className="fixed z-[1000]">
<SelectGroup>
{embeddingModelNameList.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</FormControl>
<FormDescription>
{t('knowledge.embeddingModelDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</form>
</Form>
</>
);
}

View File

@@ -3,32 +3,102 @@
import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent';
import styles from './knowledgeBase.module.css';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { KnowledgeBaseVO } from '@/app/home/knowledge/components/kb-card/KBCardVO';
import KBCard from '@/app/home/knowledge/components/kb-card/KBCard';
import KBDetailDialog from '@/app/home/knowledge/KBDetailDialog';
import { httpClient } from '@/app/infra/http/HttpClient';
import { KnowledgeBase } from '@/app/infra/entities/api';
export default function KnowledgePage() {
const { t } = useTranslation();
const [knowledgeBaseList, setKnowledgeBaseList] = useState<KnowledgeBaseVO[]>(
[],
);
const [selectedKbId, setSelectedKbId] = useState<string>('');
const [detailDialogOpen, setDetailDialogOpen] = useState(false);
useEffect(() => {
getKnowledgeBaseList();
}, []);
async function getKnowledgeBaseList() {
const resp = await httpClient.getKnowledgeBases();
setKnowledgeBaseList(
resp.bases.map((kb: KnowledgeBase) => {
const currentTime = new Date();
const lastUpdatedTimeAgo = Math.floor(
(currentTime.getTime() -
new Date(kb.updated_at ?? currentTime.getTime()).getTime()) /
1000 /
60 /
60 /
24,
);
const lastUpdatedTimeAgoText =
lastUpdatedTimeAgo > 0
? ` ${lastUpdatedTimeAgo} ${t('knowledge.daysAgo')}`
: t('knowledge.today');
return new KnowledgeBaseVO({
id: kb.uuid || '',
name: kb.name,
description: kb.description,
embeddingModelUUID: kb.embedding_model_uuid,
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
});
}),
);
}
const handleKBCardClick = (kbId: string) => {
// setIsEditForm(false);
// setModalOpen(true);
setSelectedKbId(kbId);
setDetailDialogOpen(true);
};
const handleCreateKBClick = () => {
setSelectedKbId('');
setDetailDialogOpen(true);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleFormSubmit = (value: any) => {
console.log('handleFormSubmit', value);
};
const handleFormCancel = () => {
setDetailDialogOpen(false);
};
const handleKbDeleted = () => {
getKnowledgeBaseList();
setDetailDialogOpen(false);
};
const handleNewKbCreated = () => {
getKnowledgeBaseList();
setDetailDialogOpen(false);
};
return (
<div>
<KBDetailDialog
open={detailDialogOpen}
onOpenChange={setDetailDialogOpen}
kbId={selectedKbId || undefined}
onFormSubmit={handleFormSubmit}
onFormCancel={handleFormCancel}
onKbDeleted={handleKbDeleted}
onNewKbCreated={handleNewKbCreated}
/>
<div className={styles.knowledgeListContainer}>
<CreateCardComponent
width={'100%'}
height={'10rem'}
plusSize={'90px'}
onClick={() => {
// setIsEditForm(false);
// setModalOpen(true);
}}
onClick={handleCreateKBClick}
/>
{knowledgeBaseList.map((kb) => {

View File

@@ -232,9 +232,23 @@ const enUS = {
},
knowledge: {
title: 'Knowledge',
createKnowledgeBase: 'Create Knowledge Base',
description: 'Configuring knowledge bases for improved LLM responses',
metadata: 'Metadata',
files: 'Files',
documents: 'Documents',
kbNameRequired: 'Knowledge base name cannot be empty',
kbDescriptionRequired: 'Knowledge base description cannot be empty',
embeddingModelUUIDRequired: 'Embedding model cannot be empty',
daysAgo: 'days ago',
today: 'Today',
kbName: 'Knowledge Base Name',
kbDescription: 'Knowledge Base Description',
defaultDescription: 'A knowledge base',
embeddingModelUUID: 'Embedding Model',
selectEmbeddingModel: 'Select Embedding Model',
embeddingModelDescription:
'Used to vectorize the text, you can configure it in the Models page',
updateTime: 'Updated ',
},
register: {
title: 'Initialize LangBot 👋',