perf: add notification toasts

This commit is contained in:
Junyan Qin
2025-05-09 22:36:13 +08:00
parent 337090e7cb
commit df700ec7c2
17 changed files with 131 additions and 71 deletions

22
web/package-lock.json generated
View File

@@ -27,10 +27,12 @@
"lodash": "^4.17.21",
"lucide-react": "^0.507.0",
"next": "15.2.4",
"next-themes": "^0.4.6",
"postcss": "^8.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5",
"uuidjs": "^5.1.0",
@@ -5834,6 +5836,16 @@
}
}
},
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -7443,6 +7455,16 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/sonner": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz",
"integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View File

@@ -30,10 +30,12 @@
"lodash": "^4.17.21",
"lucide-react": "^0.507.0",
"next": "15.2.4",
"next-themes": "^0.4.6",
"postcss": "^8.5.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5",
"uuidjs": "^5.1.0",

View File

@@ -14,6 +14,7 @@ import { Bot } from '@/app/infra/entities/api';
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { toast } from "sonner"
import {
Dialog,
@@ -44,8 +45,8 @@ const formSchema = z.object({
description: z.string().min(1, { message: '机器人描述不能为空' }),
adapter: z.string().min(1, { message: '适配器不能为空' }),
adapter_config: z.record(z.string(), z.any()),
enable: z.boolean(),
use_pipeline_uuid: z.string().min(1, { message: '流水线不能为空' }),
enable: z.boolean().optional(),
use_pipeline_uuid: z.string().optional(),
});
export default function BotForm({
@@ -114,6 +115,8 @@ export default function BotForm({
console.log('form', form.getValues());
handleAdapterSelect(val.adapter);
// dynamicForm.setFieldsValue(val.adapter_config);
}).catch((err) => {
toast.error("获取机器人配置失败:" + err.message);
});
} else {
@@ -182,25 +185,30 @@ export default function BotForm({
});
setAdapterNameToDynamicConfigMap(adapterNameToDynamicConfigMap);
}
async function getBotConfig(botId: string): Promise<z.infer<typeof formSchema>> {
const bot = (await httpClient.getBot(botId)).bot;
return {
adapter: bot.adapter,
description: bot.description,
name: bot.name,
adapter_config: bot.adapter_config,
enable: bot.enable ?? true,
use_pipeline_uuid: bot.use_pipeline_uuid ?? '',
};
return new Promise((resolve, reject) => {
httpClient.getBot(botId)
.then(res => {
const bot = res.bot;
resolve({
adapter: bot.adapter,
description: bot.description,
name: bot.name,
adapter_config: bot.adapter_config,
enable: bot.enable ?? true,
use_pipeline_uuid: bot.use_pipeline_uuid ?? '',
});
})
.catch(err => {
reject(err);
});
});
}
function handleAdapterSelect(adapterName: string) {
console.log('Select adapter: ', adapterName);
if (adapterName) {
const dynamicFormConfigList =
adapterNameToDynamicConfigMap.get(adapterName);
console.log(dynamicFormConfigList);
if (dynamicFormConfigList) {
setDynamicFormConfigList(dynamicFormConfigList);
if (!initBotId) {
@@ -240,20 +248,12 @@ export default function BotForm({
httpClient
.updateBot(initBotId, updateBot)
.then((res) => {
// TODO success toast
console.log('update bot success', res);
onFormSubmit(form.getValues());
// notification.success({
// message: '更新成功',
// description: '机器人更新成功',
// });
toast.success("保存成功");
})
.catch(() => {
// TODO error toast
// notification.error({
// message: '更新失败',
// description: '机器人更新失败',
// });
.catch((err) => {
toast.error("保存失败:" + err.message);
})
.finally(() => {
setIsLoading(false);
@@ -272,20 +272,12 @@ export default function BotForm({
httpClient
.createBot(newBot)
.then((res) => {
// TODO success toast
// notification.success({
// message: '创建成功',
// description: '机器人创建成功',
// });
console.log(res);
onFormSubmit(form.getValues());
toast.success("创建成功");
})
.catch(() => {
// TODO error toast
// notification.error({
// message: '创建失败',
// description: '机器人创建失败',
// });
.catch((err) => {
toast.error("创建失败:" + err.message);
})
.finally(() => {
setIsLoading(false);
@@ -295,8 +287,6 @@ export default function BotForm({
}
setShowDynamicForm(false);
console.log('set loading', false);
// TODO 刷新bot列表
// TODO 关闭当前弹窗 Already closed @setShowDynamicForm(false)?
}
function handleSaveButton() {
@@ -307,6 +297,8 @@ export default function BotForm({
if (initBotId) {
httpClient.deleteBot(initBotId).then(() => {
onBotDeleted();
}).catch((err) => {
toast.error("删除失败:" + err.message);
});
}
}

View File

@@ -18,7 +18,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { toast } from "sonner";
export default function BotConfigPage() {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [botList, setBotList] = useState<BotCardVO[]>([]);
@@ -56,12 +56,7 @@ export default function BotConfigPage() {
})
.catch((err) => {
console.error('get bot list error', err);
// TODO HACK: need refactor to hook mode Notification, but it's not working under render
// notification.error({
// message: '获取机器人列表失败',
// description: err.message,
// placement: 'bottomRight',
// });
toast.error("获取机器人列表失败:" + err.message);
})
.finally(() => {
// setIsLoading(false);

View File

@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react";
import { httpClient } from "@/app/infra/http/HttpClient";
import { LLMModel } from "@/app/infra/entities/api";
import { toast } from "sonner";
export default function DynamicFormItemComponent({
config,
@@ -25,7 +26,7 @@ export default function DynamicFormItemComponent({
httpClient.getProviderLLMModels().then((resp) => {
setLlmModels(resp.models);
}).catch((err) => {
console.error('获取 LLM 模型列表失败:', err);
toast.error("获取 LLM 模型列表失败" + err.message);
});
}
}, [config.type]);

View File

@@ -1,5 +1,5 @@
import { GetMetaDataResponse } from '@/app/infra/api/api-types/pipelines/GetMetaDataResponse';
import { ApiResponse } from '@/app/infra/api/api-types';
import { ApiResponse } from '@/app/infra/entities/api';
export async function fetchPipelineMetaData(): Promise<
ApiResponse<GetMetaDataResponse>

View File

@@ -31,7 +31,7 @@ import {
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
import { toast } from "sonner"
const extraArgSchema = z.object({
key: z.string().min(1, { message: '键名不能为空' }),
type: z.enum(['string', 'number', 'boolean']),
@@ -242,6 +242,9 @@ export default function LLMForm({
};
httpClient.createProviderLLMModel(requestParam).then(() => {
onFormSubmit(value);
toast.success("创建成功");
}).catch((err) => {
toast.error("创建失败:" + err.message);
});
}
@@ -251,6 +254,9 @@ export default function LLMForm({
if (initLLMId) {
httpClient.deleteProviderLLMModel(initLLMId).then(() => {
onLLMDeleted();
toast.success("删除成功");
}).catch((err) => {
toast.error("删除失败:" + err.message);
});
}
}

View File

@@ -17,7 +17,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { toast } from "sonner";
export default function LLMConfigPage() {
const [cardList, setCardList] = useState<LLMCardVO[]>([]);
@@ -56,8 +56,8 @@ export default function LLMConfigPage() {
setCardList(llmModelList);
})
.catch((err) => {
// TODO error toast
console.error('get LLM model list error', err);
toast.error("获取模型列表失败:" + err.message);
});
}

View File

@@ -18,7 +18,7 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { toast } from "sonner"
export default function PipelineFormComponent({
initValues,
@@ -142,6 +142,9 @@ export default function PipelineFormComponent({
httpClient.createPipeline(pipeline).then((resp) => {
onFinish();
onNewPipelineCreated(resp.uuid);
toast.success("创建成功 请编辑流水线详细参数");
}).catch((err) => {
toast.error("创建失败:" + err.message);
});
}
@@ -165,7 +168,12 @@ export default function PipelineFormComponent({
// uuid: pipelineId || '',
// is_default: false,
};
httpClient.updatePipeline(pipelineId || '', pipeline).then(() => onFinish());
httpClient.updatePipeline(pipelineId || '', pipeline).then(() => {
onFinish();
toast.success("保存成功");
}).catch((err) => {
toast.error("保存失败:" + err.message);
});
}
function renderDynamicForms(stage: PipelineConfigStage, formName: keyof FormValues) {

View File

@@ -16,7 +16,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog"
import { Button } from '@/components/ui/button';
import { toast } from "sonner"
export default function PluginConfigPage() {
const [modalOpen, setModalOpen] = useState<boolean>(false);
const [isEditForm, setIsEditForm] = useState(false);
@@ -57,8 +57,8 @@ export default function PluginConfigPage() {
setPipelineList(pipelineList);
})
.catch((error) => {
// TODO toast
console.log(error);
toast.error("获取流水线列表失败:" + error.message);
});
}

View File

@@ -4,6 +4,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
import { Badge } from "@/components/ui/badge"
import { Switch } from "@/components/ui/switch"
import { Button } from "@/components/ui/button"
import { toast } from "sonner"
export default function PluginCardComponent({
cardVO,
@@ -24,7 +25,7 @@ export default function PluginCardComponent({
setEnabled(!enabled);
})
.catch((err) => {
console.log('error: ', err);
toast.error("修改失败:" + err.message);
})
.finally(() => {
setSwitchEnable(true);

View File

@@ -12,6 +12,7 @@ import {
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { toast } from "sonner";
enum PluginRemoveStatus {
WAIT_INPUT = 'WAIT_INPUT',
@@ -51,14 +52,14 @@ export default function PluginForm({
const handleSubmit = async (values: object) => {
setIsLoading(true);
try {
await httpClient.updatePluginConfig(pluginAuthor, pluginName, values);
onFormSubmit();
} catch (error) {
console.error('更新插件配置失败:', error);
} finally {
setIsLoading(false);
}
httpClient.updatePluginConfig(pluginAuthor, pluginName, values).then(() => {
onFormSubmit();
toast.success("保存成功");
}).catch((error) => {
toast.error("保存失败:" + error.message);
}).finally(() => {
setIsLoading(false);
});
};
if (!pluginInfo || !pluginConfig) {

View File

@@ -30,7 +30,8 @@ import {
GetPipelineMetadataResponseData,
AsyncTask
} from '@/app/infra/entities/api';
import { notification } from 'antd';
import { toast } from "sonner"
type JSONValue = string | number | boolean | JSONObject | JSONArray | null;
interface JSONObject {
@@ -141,13 +142,8 @@ class HttpClient {
console.error('Permission denied:', errMessage);
break;
case 500:
// TODO 弹Toast窗
// NOTE: move to component layer for customized message?
notification.error({
message: '服务器错误',
description: errMessage,
placement: 'bottomRight',
});
// toast.error(errMessage);
console.error('Server error:', errMessage);
break;
}

View File

@@ -1,5 +1,6 @@
import './global.css';
import type { Metadata } from 'next';
import { Toaster } from '@/components/ui/sonner';
export const metadata: Metadata = {
title: 'LangBot',
@@ -13,7 +14,10 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={``}>{children}</body>
<body className={``}>
{children}
<Toaster />
</body>
</html>
);
}

View File

@@ -20,6 +20,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
import { useRouter } from 'next/navigation';
import { Mail, Lock } from "lucide-react";
import langbotIcon from '@/app/assets/langbot-logo.webp';
import { toast } from "sonner"
const formSchema = z.object({
email: z.string().email("请输入有效的邮箱地址"),
@@ -78,9 +79,12 @@ export default function Login() {
localStorage.setItem('token', res.token);
console.log('login success: ', res);
router.push('/home');
toast.success("登录成功");
})
.catch((err) => {
console.log('login error: ', err);
toast.error("登录失败,请检查邮箱和密码是否正确");
});
}

View File

@@ -21,6 +21,7 @@ import { httpClient } from '@/app/infra/http/HttpClient';
import { useRouter } from 'next/navigation';
import { Mail, Lock } from "lucide-react";
import langbotIcon from '@/app/assets/langbot-logo.webp';
import { toast } from "sonner";
const formSchema = z.object({
email: z.string().email("请输入有效的邮箱地址"),
@@ -64,10 +65,12 @@ export default function Register() {
.initUser(username, password)
.then((res) => {
console.log('init user success: ', res);
toast.success("初始化成功 请登录");
router.push('/login');
})
.catch((err) => {
console.log('init user error: ', err);
toast.error("初始化失败:" + err.message);
});
}

View File

@@ -0,0 +1,25 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }