feat: implement models dialog for managing LLM and embedding models with dynamic URL handling

This commit is contained in:
Junyan Qin
2025-12-25 20:54:00 +08:00
parent 10ee30695a
commit 7479545339
16 changed files with 175 additions and 115 deletions

View File

@@ -6,7 +6,7 @@ import {
SidebarChild,
SidebarChildVO,
} from '@/app/home/components/home-sidebar/HomeSidebarChild';
import { useRouter, usePathname } from 'next/navigation';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { sidebarConfigList } from '@/app/home/components/home-sidebar/sidbarConfigList';
import langbotIcon from '@/app/assets/langbot-logo.webp';
import { systemInfo } from '@/app/infra/http/HttpClient';
@@ -36,6 +36,7 @@ import { Badge } from '@/components/ui/badge';
import PasswordChangeDialog from '@/app/home/components/password-change-dialog/PasswordChangeDialog';
import ApiIntegrationDialog from '@/app/home/components/api-integration-dialog/ApiIntegrationDialog';
import NewVersionDialog from '@/app/home/components/new-version-dialog/NewVersionDialog';
import ModelsDialog from '@/app/home/components/models-dialog/ModelsDialog';
import { GitHubRelease } from '@/app/infra/http/CloudServiceClient';
// Compare two version strings, returns true if v1 > v2
@@ -67,11 +68,19 @@ export default function HomeSidebar({
// 路由相关
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// 路由被动变化时处理
useEffect(() => {
handleRouteChange(pathname);
}, [pathname]);
// 检查 URL 参数,自动打开模型对话框
useEffect(() => {
if (searchParams.get('action') === 'showModelSettings') {
setModelsDialogOpen(true);
}
}, [searchParams]);
const [selectedChild, setSelectedChild] = useState<SidebarChildVO>();
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
@@ -85,6 +94,24 @@ export default function HomeSidebar({
);
const [hasNewVersion, setHasNewVersion] = useState(false);
const [versionDialogOpen, setVersionDialogOpen] = useState(false);
const [modelsDialogOpen, setModelsDialogOpen] = useState(false);
// 处理模型对话框的打开和关闭,同时更新 URL
function handleModelsDialogChange(open: boolean) {
setModelsDialogOpen(open);
if (open) {
const params = new URLSearchParams(searchParams.toString());
params.set('action', 'showModelSettings');
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
} else {
const params = new URLSearchParams(searchParams.toString());
params.delete('action');
const newUrl = params.toString()
? `${pathname}?${params.toString()}`
: pathname;
router.replace(newUrl, { scroll: false });
}
}
useEffect(() => {
initSelect();
@@ -252,6 +279,21 @@ export default function HomeSidebar({
</div>
)}
<SidebarChild
onClick={() => handleModelsDialogChange(true)}
isSelected={false}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M10.6144 17.7956C10.277 18.5682 9.20776 18.5682 8.8704 17.7956L7.99275 15.7854C7.21171 13.9966 5.80589 12.5726 4.0523 11.7942L1.63658 10.7219C.868536 10.381.868537 9.26368 1.63658 8.92276L3.97685 7.88394C5.77553 7.08552 7.20657 5.60881 7.97427 3.75892L8.8633 1.61673C9.19319.821767 10.2916.821765 10.6215 1.61673L11.5105 3.75894C12.2782 5.60881 13.7092 7.08552 15.5079 7.88394L17.8482 8.92276C18.6162 9.26368 18.6162 10.381 17.8482 10.7219L15.4325 11.7942C13.6789 12.5726 12.2731 13.9966 11.492 15.7854L10.6144 17.7956ZM4.53956 9.82234C6.8254 10.837 8.68402 12.5048 9.74238 14.7996 10.8008 12.5048 12.6594 10.837 14.9452 9.82234 12.6321 8.79557 10.7676 7.04647 9.74239 4.71088 8.71719 7.04648 6.85267 8.79557 4.53956 9.82234ZM19.4014 22.6899 19.6482 22.1242C20.0882 21.1156 20.8807 20.3125 21.8695 19.8732L22.6299 19.5353C23.0412 19.3526 23.0412 18.7549 22.6299 18.5722L21.9121 18.2532C20.8978 17.8026 20.0911 16.9698 19.6586 15.9269L19.4052 15.3156C19.2285 14.8896 18.6395 14.8896 18.4628 15.3156L18.2094 15.9269C17.777 16.9698 16.9703 17.8026 15.956 18.2532L15.2381 18.5722C14.8269 18.7549 14.8269 19.3526 15.2381 19.5353L15.9985 19.8732C16.9874 20.3125 17.7798 21.1156 18.2198 22.1242L18.4667 22.6899C18.6473 23.104 19.2207 23.104 19.4014 22.6899ZM18.3745 19.0469 18.937 18.4883 19.4878 19.0469 18.937 19.5898 18.3745 19.0469Z"></path>
</svg>
}
name={t('models.title')}
/>
<Popover
open={popoverOpen}
onOpenChange={(open) => {
@@ -414,6 +456,10 @@ export default function HomeSidebar({
onOpenChange={setVersionDialogOpen}
release={latestRelease}
/>
<ModelsDialog
open={modelsDialogOpen}
onOpenChange={handleModelsDialogChange}
/>
</div>
);
}

View File

@@ -27,27 +27,6 @@ export const sidebarConfigList = [
zh_Hans: 'https://docs.langbot.app/zh/deploy/platforms/readme.html',
},
}),
new SidebarChildVO({
id: 'models',
name: t('models.title'),
icon: (
<svg
className={`${styles.sidebarChildIcon}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M10.6144 17.7956C10.277 18.5682 9.20776 18.5682 8.8704 17.7956L7.99275 15.7854C7.21171 13.9966 5.80589 12.5726 4.0523 11.7942L1.63658 10.7219C.868536 10.381.868537 9.26368 1.63658 8.92276L3.97685 7.88394C5.77553 7.08552 7.20657 5.60881 7.97427 3.75892L8.8633 1.61673C9.19319.821767 10.2916.821765 10.6215 1.61673L11.5105 3.75894C12.2782 5.60881 13.7092 7.08552 15.5079 7.88394L17.8482 8.92276C18.6162 9.26368 18.6162 10.381 17.8482 10.7219L15.4325 11.7942C13.6789 12.5726 12.2731 13.9966 11.492 15.7854L10.6144 17.7956ZM4.53956 9.82234C6.8254 10.837 8.68402 12.5048 9.74238 14.7996 10.8008 12.5048 12.6594 10.837 14.9452 9.82234 12.6321 8.79557 10.7676 7.04647 9.74239 4.71088 8.71719 7.04648 6.85267 8.79557 4.53956 9.82234ZM19.4014 22.6899 19.6482 22.1242C20.0882 21.1156 20.8807 20.3125 21.8695 19.8732L22.6299 19.5353C23.0412 19.3526 23.0412 18.7549 22.6299 18.5722L21.9121 18.2532C20.8978 17.8026 20.0911 16.9698 19.6586 15.9269L19.4052 15.3156C19.2285 14.8896 18.6395 14.8896 18.4628 15.3156L18.2094 15.9269C17.777 16.9698 16.9703 17.8026 15.956 18.2532L15.2381 18.5722C14.8269 18.7549 14.8269 19.3526 15.2381 19.5353L15.9985 19.8732C16.9874 20.3125 17.7798 21.1156 18.2198 22.1242L18.4667 22.6899C18.6473 23.104 19.2207 23.104 19.4014 22.6899ZM18.3745 19.0469 18.937 18.4883 19.4878 19.0469 18.937 19.5898 18.3745 19.0469Z"></path>
</svg>
),
route: '/home/models',
description: t('models.description'),
helpLink: {
en_US: 'https://docs.langbot.app/en/deploy/models/readme.html',
zh_Hans: 'https://docs.langbot.app/zh/deploy/models/readme.html',
},
}),
new SidebarChildVO({
id: 'pipelines',
name: t('pipelines.title'),

View File

@@ -1,11 +1,10 @@
'use client';
import { useState, useEffect } from 'react';
import { LLMCardVO } from '@/app/home/models/component/llm-card/LLMCardVO';
import styles from './LLMConfig.module.css';
import LLMCard from '@/app/home/models/component/llm-card/LLMCard';
import LLMForm from '@/app/home/models/component/llm-form/LLMForm';
import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent';
import { Plus, MessageSquareText, Cpu, Info } from 'lucide-react';
import { LLMCardVO } from './component/llm-card/LLMCardVO';
import LLMCard from './component/llm-card/LLMCard';
import LLMForm from './component/llm-form/LLMForm';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LLMModel } from '@/app/infra/entities/api';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -15,15 +14,25 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import { extractI18nObject } from '@/i18n/I18nProvider';
import { EmbeddingCardVO } from '@/app/home/models/component/embedding-card/EmbeddingCardVO';
import EmbeddingCard from '@/app/home/models/component/embedding-card/EmbeddingCard';
import EmbeddingForm from '@/app/home/models/component/embedding-form/EmbeddingForm';
import { EmbeddingCardVO } from './component/embedding-card/EmbeddingCardVO';
import EmbeddingCard from './component/embedding-card/EmbeddingCard';
import EmbeddingForm from './component/embedding-form/EmbeddingForm';
export default function LLMConfigPage() {
interface ModelsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export default function ModelsDialog({
open,
onOpenChange,
}: ModelsDialogProps) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<string>('llm');
const [cardList, setCardList] = useState<LLMCardVO[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [isEditForm, setIsEditForm] = useState(false);
@@ -37,9 +46,11 @@ export default function LLMConfigPage() {
useState<EmbeddingCardVO | null>(null);
useEffect(() => {
getLLMModelList();
getEmbeddingModelList();
}, []);
if (open) {
getLLMModelList();
getEmbeddingModelList();
}
}, [open]);
async function getLLMModelList() {
const requesterNameListResp = await httpClient.getProviderRequesters('llm');
@@ -134,7 +145,108 @@ export default function LLMConfigPage() {
}
return (
<div>
<>
<Dialog
open={open}
onOpenChange={(newOpen) => {
if (!newOpen && (modalOpen || embeddingModalOpen)) {
return;
}
onOpenChange(newOpen);
}}
>
<DialogContent className="overflow-hidden p-0 !max-w-[80vw] h-[75vh] flex flex-col">
<DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle>{t('models.title')}</DialogTitle>
</DialogHeader>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="w-full flex-1 flex flex-col overflow-hidden px-6 pb-6"
>
<div className="flex flex-row justify-between items-center mb-2 mt-4">
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
<TabsTrigger value="llm" className="px-6 py-4 cursor-pointer">
<MessageSquareText className="h-4 w-4 mr-1.5" />
{t('llm.llmModels')}
</TabsTrigger>
<TabsTrigger
value="embedding"
className="px-6 py-4 cursor-pointer"
>
<Cpu className="h-4 w-4 mr-1.5" />
{t('embedding.embeddingModels')}
</TabsTrigger>
</TabsList>
<Button
size="sm"
onClick={
activeTab === 'llm'
? handleCreateModelClick
: handleCreateEmbeddingModelClick
}
>
<Plus className="h-4 w-4 mr-1" />
{activeTab === 'llm'
? t('models.createModel')
: t('embedding.createModel')}
</Button>
</div>
<div className="mb-3 flex items-center">
<Info className="h-4 w-4 mr-1.5 text-muted-foreground" />
{activeTab === 'llm' ? (
<p className="text-sm text-muted-foreground flex items-center">
{t('llm.description')}
</p>
) : (
<p className="text-sm text-muted-foreground flex items-center">
{t('embedding.description')}
</p>
)}
</div>
<TabsContent value="llm" className="flex-1 overflow-auto mt-0">
<div className="w-full grid grid-cols-[repeat(auto-fill,minmax(20rem,1fr))] gap-4">
{cardList.map((cardVO) => {
return (
<div
key={cardVO.id}
onClick={() => {
selectLLM(cardVO);
}}
>
<LLMCard cardVO={cardVO}></LLMCard>
</div>
);
})}
</div>
</TabsContent>
<TabsContent
value="embedding"
className="flex-1 overflow-auto mt-0"
>
<div className="w-full grid grid-cols-[repeat(auto-fill,minmax(20rem,1fr))] gap-4">
{embeddingCardList.map((cardVO) => {
return (
<div
key={cardVO.id}
onClick={() => {
selectEmbedding(cardVO);
}}
>
<EmbeddingCard cardVO={cardVO}></EmbeddingCard>
</div>
);
})}
</div>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogContent className="w-[700px] p-6">
<DialogHeader>
@@ -159,6 +271,7 @@ export default function LLMConfigPage() {
/>
</DialogContent>
</Dialog>
<Dialog open={embeddingModalOpen} onOpenChange={setEmbeddingModalOpen}>
<DialogContent className="w-[700px] p-6">
<DialogHeader>
@@ -185,84 +298,6 @@ export default function LLMConfigPage() {
/>
</DialogContent>
</Dialog>
<Tabs defaultValue="llm" className="w-full">
<div className="flex flex-row gap-0 mb-4">
<div className="flex flex-row justify-between items-center px-[0.8rem]">
<TabsList className="shadow-md py-5 bg-[#f0f0f0] dark:bg-[#2a2a2e]">
<TabsTrigger value="llm" className="px-6 py-4 cursor-pointer">
{t('llm.llmModels')}
</TabsTrigger>
<TabsTrigger
value="embedding"
className="px-6 py-4 cursor-pointer"
>
{t('embedding.embeddingModels')}
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="llm">
<div className="flex flex-row justify-between items-center px-[0.4rem] h-full">
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('llm.description')}
</p>
</div>
</TabsContent>
<TabsContent value="embedding">
<div className="flex flex-row justify-between items-center px-[0.4rem] h-full">
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('embedding.description')}
</p>
</div>
</TabsContent>
</div>
<TabsContent value="llm">
<div className={`${styles.modelListContainer}`}>
<CreateCardComponent
width={'100%'}
height={'10rem'}
plusSize={'90px'}
onClick={handleCreateModelClick}
/>
{cardList.map((cardVO) => {
return (
<div
key={cardVO.id}
onClick={() => {
selectLLM(cardVO);
}}
>
<LLMCard cardVO={cardVO}></LLMCard>
</div>
);
})}
</div>
</TabsContent>
<TabsContent value="embedding">
<div className={`${styles.modelListContainer}`}>
<CreateCardComponent
width={'100%'}
height={'10rem'}
plusSize={'90px'}
onClick={handleCreateEmbeddingModelClick}
/>
{embeddingCardList.map((cardVO) => {
return (
<div
key={cardVO.id}
onClick={() => {
selectEmbedding(cardVO);
}}
>
<EmbeddingCard cardVO={cardVO}></EmbeddingCard>
</div>
);
})}
</div>
</TabsContent>
</Tabs>
</div>
</>
);
}

View File

@@ -104,7 +104,7 @@ const zhHans = {
models: {
title: '模型配置',
description: '配置和管理可在流水线中使用的模型',
createModel: '创建模型',
createModel: '创建对话模型',
editModel: '编辑模型',
getModelListError: '获取模型列表失败:',
modelName: '模型名称',