feat: complete tool and model restart feedback

This commit is contained in:
SiYue-ZO
2026-04-25 02:03:26 +08:00
parent 02d9a0d190
commit cbe6a0907c
5 changed files with 71 additions and 13 deletions
@@ -1,5 +1,6 @@
import { useLayoutEffect, useRef } from "react"
import { useTranslation } from "react-i18next"
import { PageHeader } from "@/components/page-header"
import { ToolLibraryTab } from "./tool-library-tab"
@@ -26,6 +27,7 @@ export function ToolsPage() {
isToolsLoading,
isWebSearchLoading,
isWebSearchSaving,
isWebSearchDirty,
setActiveTab,
setSearchQuery,
setStatusFilter,
@@ -72,6 +74,7 @@ export function ToolsPage() {
isLoading={isWebSearchLoading}
hasError={hasWebSearchError}
isSaving={isWebSearchSaving}
isDirty={isWebSearchDirty}
onSave={saveWebSearchConfig}
onToggleProviderExpand={toggleExpandedProvider}
onUpdateDraft={updateWebSearchDraft}
@@ -4,12 +4,13 @@ import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import {
type WebSearchConfigResponse,
getTools,
getWebSearchConfig,
setToolEnabled,
updateWebSearchConfig,
type WebSearchConfigResponse,
} from "@/api/tools"
import { showSaveSuccessOrRestartToast } from "@/lib/restart-required"
import { refreshGatewayState } from "@/store/gateway"
import type { GroupedTools, ToolStatusFilter, ToolsPageTab } from "./types"
@@ -35,24 +36,38 @@ export function useToolsPage() {
queryFn: getWebSearchConfig,
})
const tools = useMemo(() => toolsQuery.data?.tools ?? [], [toolsQuery.data?.tools])
const tools = useMemo(
() => toolsQuery.data?.tools ?? [],
[toolsQuery.data?.tools],
)
const normalizedSearchQuery = deferredSearchQuery.trim().toLowerCase()
const webSearchDraft = webSearchDraftOverride ?? webSearchQuery.data ?? null
const isWebSearchDirty = useMemo(() => {
if (!webSearchDraft || !webSearchQuery.data) {
return false
}
return (
JSON.stringify(webSearchDraft) !== JSON.stringify(webSearchQuery.data)
)
}, [webSearchDraft, webSearchQuery.data])
const toggleToolMutation = useMutation({
mutationFn: async ({ name, enabled }: { name: string; enabled: boolean }) =>
setToolEnabled(name, enabled),
onSuccess: (_, variables) => {
toast.success(
onSuccess: async (_, variables) => {
const gateway = await refreshGatewayState({ force: true })
showSaveSuccessOrRestartToast(
t,
variables.enabled
? t("pages.agent.tools.enable_success", "Tool enabled successfully")
: t(
"pages.agent.tools.disable_success",
"Tool disabled successfully",
),
t("navigation.tools", "Tools"),
gateway?.restartRequired === true,
)
void queryClient.invalidateQueries({ queryKey: ["tools"] })
void refreshGatewayState({ force: true })
},
onError: (error) => {
toast.error(
@@ -65,20 +80,23 @@ export function useToolsPage() {
const saveWebSearchMutation = useMutation({
mutationFn: updateWebSearchConfig,
onSuccess: (updatedConfig) => {
onSuccess: async (updatedConfig) => {
queryClient.setQueryData(["tools", "web-search-config"], updatedConfig)
setWebSearchDraftOverride(null)
toast.success(
const gateway = await refreshGatewayState({ force: true })
showSaveSuccessOrRestartToast(
t,
t(
"pages.agent.tools.web_search.save_success",
"Settings saved successfully",
),
t("pages.agent.tools.web_search.title", "Web Search Configuration"),
gateway?.restartRequired === true,
)
void queryClient.invalidateQueries({
queryKey: ["tools", "web-search-config"],
})
void queryClient.invalidateQueries({ queryKey: ["tools"] })
void refreshGatewayState({ force: true })
},
onError: (error) => {
toast.error(
@@ -105,7 +123,9 @@ export function useToolsPage() {
}
if (normalizedSearchQuery) {
const matchesName = tool.name.toLowerCase().includes(normalizedSearchQuery)
const matchesName = tool.name
.toLowerCase()
.includes(normalizedSearchQuery)
const matchesDescription = (tool.description || "")
.toLowerCase()
.includes(normalizedSearchQuery)
@@ -177,6 +197,7 @@ export function useToolsPage() {
isToolsLoading: toolsQuery.isLoading,
isWebSearchLoading: webSearchQuery.isLoading,
isWebSearchSaving: saveWebSearchMutation.isPending,
isWebSearchDirty,
setActiveTab,
setSearchQuery,
setStatusFilter,
@@ -1,6 +1,7 @@
import { useTranslation } from "react-i18next"
import type { WebSearchConfigResponse } from "@/api/tools"
import { ConfigChangeNotice } from "@/components/config-change-notice"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
@@ -15,6 +16,7 @@ interface WebSearchTabProps {
isLoading: boolean
hasError: boolean
isSaving: boolean
isDirty: boolean
onSave: () => void
onToggleProviderExpand: (providerId: string) => void
onUpdateDraft: WebSearchDraftUpdater
@@ -27,6 +29,7 @@ export function WebSearchTab({
isLoading,
hasError,
isSaving,
isDirty,
onSave,
onToggleProviderExpand,
onUpdateDraft,
@@ -66,13 +69,21 @@ export function WebSearchTab({
<Button
onClick={onSave}
disabled={isSaving}
disabled={!isDirty || isSaving}
className="h-10 shrink-0 rounded-xl px-6 shadow-sm transition-all active:scale-95"
>
{t("pages.agent.tools.web_search.save", "Save Changes")}
</Button>
</div>
{isDirty && (
<ConfigChangeNotice
kind="save"
title={t("common.saveChangesTitle")}
description={t("pages.agent.tools.web_search.unsaved_prompt")}
/>
)}
<div className="space-y-10">
<WebSearchGeneralSettings
draft={draft}
@@ -1,10 +1,13 @@
import { IconLoader2, IconPlus, IconStar } from "@tabler/icons-react"
import { useCallback, useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { type ModelInfo, getModels, setDefaultModel } from "@/api/models"
import { PageHeader } from "@/components/page-header"
import { Button } from "@/components/ui/button"
import { showSaveSuccessOrRestartToast } from "@/lib/restart-required"
import { refreshGatewayState } from "@/store/gateway"
import { AddModelSheet } from "./add-model-sheet"
import { DeleteModelDialog } from "./delete-model-dialog"
@@ -95,8 +98,15 @@ export function ModelsPage() {
try {
await setDefaultModel(model.model_name)
await fetchModels()
} catch {
// ignore
const gateway = await refreshGatewayState({ force: true })
showSaveSuccessOrRestartToast(
t,
t("models.defaultChangeSuccess"),
model.model_name,
gateway?.restartRequired === true,
)
} catch (e) {
toast.error(e instanceof Error ? e.message : t("models.loadError"))
} finally {
setSettingDefaultIndex(null)
}
+14 -1
View File
@@ -1,6 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { type ModelInfo, getModels, setDefaultModel } from "@/api/models"
import { showSaveSuccessOrRestartToast } from "@/lib/restart-required"
import { refreshGatewayState } from "@/store/gateway"
interface UseChatModelsOptions {
isConnected: boolean
@@ -18,6 +22,7 @@ function isLocalModel(model: ModelInfo): boolean {
}
export function useChatModels({ isConnected }: UseChatModelsOptions) {
const { t } = useTranslation()
const [modelList, setModelList] = useState<ModelInfo[]>([])
const [defaultModelName, setDefaultModelName] = useState("")
const setDefaultRequestIdRef = useRef(0)
@@ -58,11 +63,19 @@ export function useChatModels({ isConnected }: UseChatModelsOptions) {
if (data.models.some((m) => m.model_name === data.default_model)) {
setDefaultModelName(data.default_model)
}
const gateway = await refreshGatewayState({ force: true })
showSaveSuccessOrRestartToast(
t,
t("models.defaultChangeSuccess"),
modelName,
gateway?.restartRequired === true,
)
} catch (err) {
console.error("Failed to set default model:", err)
toast.error(err instanceof Error ? err.message : t("models.loadError"))
}
},
[defaultModelName],
[defaultModelName, t],
)
const hasAvailableModels = useMemo(