From df700ec7c262066f5aee1a093108c49a83e0b84f Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Fri, 9 May 2025 22:36:13 +0800 Subject: [PATCH] perf: add notification toasts --- web/package-lock.json | 22 ++++++ web/package.json | 2 + .../home/bots/components/bot-form/BotForm.tsx | 68 ++++++++----------- web/src/app/home/bots/page.tsx | 9 +-- .../dynamic-form/DynamicFormItemComponent.tsx | 3 +- web/src/app/home/mock-api/index.ts | 2 +- .../models/component/llm-form/LLMForm.tsx | 8 ++- web/src/app/home/models/page.tsx | 4 +- .../pipeline-form/PipelineFormComponent.tsx | 12 +++- web/src/app/home/pipelines/page.tsx | 4 +- .../plugin-card/PluginCardComponent.tsx | 3 +- .../plugin-form/PluginForm.tsx | 17 ++--- web/src/app/infra/http/HttpClient.ts | 10 +-- web/src/app/layout.tsx | 6 +- web/src/app/login/page.tsx | 4 ++ web/src/app/register/page.tsx | 3 + web/src/components/ui/sonner.tsx | 25 +++++++ 17 files changed, 131 insertions(+), 71 deletions(-) create mode 100644 web/src/components/ui/sonner.tsx diff --git a/web/package-lock.json b/web/package-lock.json index 69d7c822..672d9dae 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/package.json b/web/package.json index d058d419..536bc657 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/app/home/bots/components/bot-form/BotForm.tsx b/web/src/app/home/bots/components/bot-form/BotForm.tsx index b87794fa..3cb65d88 100644 --- a/web/src/app/home/bots/components/bot-form/BotForm.tsx +++ b/web/src/app/home/bots/components/bot-form/BotForm.tsx @@ -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> { - 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); }); } } diff --git a/web/src/app/home/bots/page.tsx b/web/src/app/home/bots/page.tsx index 9a432d58..0e7d6b7c 100644 --- a/web/src/app/home/bots/page.tsx +++ b/web/src/app/home/bots/page.tsx @@ -18,7 +18,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" - +import { toast } from "sonner"; export default function BotConfigPage() { const [modalOpen, setModalOpen] = useState(false); const [botList, setBotList] = useState([]); @@ -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); diff --git a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx index bf18117d..4e654cd4 100644 --- a/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx +++ b/web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx @@ -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]); diff --git a/web/src/app/home/mock-api/index.ts b/web/src/app/home/mock-api/index.ts index 8791ecfd..faeb41bd 100644 --- a/web/src/app/home/mock-api/index.ts +++ b/web/src/app/home/mock-api/index.ts @@ -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 diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx index b6a4fee6..de46e739 100644 --- a/web/src/app/home/models/component/llm-form/LLMForm.tsx +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -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); }); } } diff --git a/web/src/app/home/models/page.tsx b/web/src/app/home/models/page.tsx index f0ca7f24..bc158d53 100644 --- a/web/src/app/home/models/page.tsx +++ b/web/src/app/home/models/page.tsx @@ -17,7 +17,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" - +import { toast } from "sonner"; export default function LLMConfigPage() { const [cardList, setCardList] = useState([]); @@ -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); }); } diff --git a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx index 2e578007..b1f391ba 100644 --- a/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx +++ b/web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx @@ -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) { diff --git a/web/src/app/home/pipelines/page.tsx b/web/src/app/home/pipelines/page.tsx index 9e15b7d8..fc1fddfe 100644 --- a/web/src/app/home/pipelines/page.tsx +++ b/web/src/app/home/pipelines/page.tsx @@ -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(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); }); } diff --git a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx index e507a471..7db55d68 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -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); diff --git a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx index f8899157..aa9c9262 100644 --- a/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx +++ b/web/src/app/home/plugins/plugin-installed/plugin-form/PluginForm.tsx @@ -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) { diff --git a/web/src/app/infra/http/HttpClient.ts b/web/src/app/infra/http/HttpClient.ts index 9243a1bb..90cdb842 100644 --- a/web/src/app/infra/http/HttpClient.ts +++ b/web/src/app/infra/http/HttpClient.ts @@ -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; } diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 6d6e7ad0..8aebbeb8 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -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 ( - {children} + + {children} + + ); } diff --git a/web/src/app/login/page.tsx b/web/src/app/login/page.tsx index afdafbdd..5d43bdcc 100644 --- a/web/src/app/login/page.tsx +++ b/web/src/app/login/page.tsx @@ -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("登录失败,请检查邮箱和密码是否正确"); }); } diff --git a/web/src/app/register/page.tsx b/web/src/app/register/page.tsx index 81e6ff8e..39ef1ba2 100644 --- a/web/src/app/register/page.tsx +++ b/web/src/app/register/page.tsx @@ -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); }); } diff --git a/web/src/components/ui/sonner.tsx b/web/src/components/ui/sonner.tsx new file mode 100644 index 00000000..957524ed --- /dev/null +++ b/web/src/components/ui/sonner.tsx @@ -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 ( + + ) +} + +export { Toaster }