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", "lodash": "^4.17.21",
"lucide-react": "^0.507.0", "lucide-react": "^0.507.0",
"next": "15.2.4", "next": "15.2.4",
"next-themes": "^0.4.6",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.3", "react-hook-form": "^7.56.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5", "tailwindcss": "^4.1.5",
"uuidjs": "^5.1.0", "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": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -7443,6 +7455,16 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "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": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "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", "lodash": "^4.17.21",
"lucide-react": "^0.507.0", "lucide-react": "^0.507.0",
"next": "15.2.4", "next": "15.2.4",
"next-themes": "^0.4.6",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.3", "react-hook-form": "^7.56.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5", "tailwindcss": "^4.1.5",
"uuidjs": "^5.1.0", "uuidjs": "^5.1.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ import {
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { toast } from "sonner"
export default function PluginConfigPage() { export default function PluginConfigPage() {
const [modalOpen, setModalOpen] = useState<boolean>(false); const [modalOpen, setModalOpen] = useState<boolean>(false);
const [isEditForm, setIsEditForm] = useState(false); const [isEditForm, setIsEditForm] = useState(false);
@@ -57,8 +57,8 @@ export default function PluginConfigPage() {
setPipelineList(pipelineList); setPipelineList(pipelineList);
}) })
.catch((error) => { .catch((error) => {
// TODO toast
console.log(error); 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 { Badge } from "@/components/ui/badge"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { toast } from "sonner"
export default function PluginCardComponent({ export default function PluginCardComponent({
cardVO, cardVO,
@@ -24,7 +25,7 @@ export default function PluginCardComponent({
setEnabled(!enabled); setEnabled(!enabled);
}) })
.catch((err) => { .catch((err) => {
console.log('error: ', err); toast.error("修改失败:" + err.message);
}) })
.finally(() => { .finally(() => {
setSwitchEnable(true); setSwitchEnable(true);

View File

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

View File

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

View File

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

View File

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