diff --git a/web/frontend/src/components/agent/tools/tools-page.tsx b/web/frontend/src/components/agent/tools/tools-page.tsx index 7d2d0fac6..c221f911c 100644 --- a/web/frontend/src/components/agent/tools/tools-page.tsx +++ b/web/frontend/src/components/agent/tools/tools-page.tsx @@ -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} diff --git a/web/frontend/src/components/agent/tools/use-tools-page.ts b/web/frontend/src/components/agent/tools/use-tools-page.ts index 07f9d50d4..ecc433b0e 100644 --- a/web/frontend/src/components/agent/tools/use-tools-page.ts +++ b/web/frontend/src/components/agent/tools/use-tools-page.ts @@ -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, diff --git a/web/frontend/src/components/agent/tools/web-search-tab.tsx b/web/frontend/src/components/agent/tools/web-search-tab.tsx index b3f9d0750..866e0f27f 100644 --- a/web/frontend/src/components/agent/tools/web-search-tab.tsx +++ b/web/frontend/src/components/agent/tools/web-search-tab.tsx @@ -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({ + {isDirty && ( + + )} +
([]) 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(