mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat: complete tool and model restart feedback
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user