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