refresh views after assistant changes
This commit is contained in:
1 parent
94ec244fad
commit
fc07e911d1
14 files changed
+343
-7
No files matched your search
@@ -16,11 +16,13 @@ describe("assistant browser tool executors", () => {
|
||||
const navigate = vi.fn();
|
||||
const openCommandMenu = vi.fn();
|
||||
const openMediaRequestSearch = vi.fn();
|
||||
const refreshCurrentView = vi.fn().mockResolvedValue(undefined);
|
||||
const executors = createAssistantBrowserToolExecutors({
|
||||
getOrigin: () => "https://homarr.example",
|
||||
navigate,
|
||||
openCommandMenu,
|
||||
openMediaRequestSearch,
|
||||
refreshCurrentView,
|
||||
});
|
||||
|
||||
await expect(executors.navigate_to_route({ path: "/manage/apps" })).resolves.toEqual({
|
||||
@@ -29,11 +31,13 @@ describe("assistant browser tool executors", () => {
|
||||
});
|
||||
await expect(executors.open_command_menu()).resolves.toEqual({ success: true });
|
||||
await expect(executors.open_media_request_search()).resolves.toEqual({ success: true });
|
||||
await expect(executors.refresh_current_view()).resolves.toEqual({ success: true });
|
||||
|
||||
expect(navigate).toHaveBeenCalledTimes(1);
|
||||
expect(navigate).toHaveBeenCalledWith("/manage/apps");
|
||||
expect(openCommandMenu).toHaveBeenCalledOnce();
|
||||
expect(openMediaRequestSearch).toHaveBeenCalledOnce();
|
||||
expect(refreshCurrentView).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("does not navigate when the assistant requests an unsafe route", async () => {
|
||||
@@ -43,6 +47,7 @@ describe("assistant browser tool executors", () => {
|
||||
navigate,
|
||||
openCommandMenu: vi.fn(),
|
||||
openMediaRequestSearch: vi.fn(),
|
||||
refreshCurrentView: vi.fn().mockResolvedValue(undefined),
|
||||
});
|
||||
|
||||
await expect(executors.navigate_to_route({ path: "https://example.com" })).resolves.toEqual({
|
||||
@@ -51,4 +56,19 @@ describe("assistant browser tool executors", () => {
|
||||
});
|
||||
expect(navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("reports a failed refresh without crashing the assistant run", async () => {
|
||||
const executors = createAssistantBrowserToolExecutors({
|
||||
getOrigin: () => "https://homarr.example",
|
||||
navigate: vi.fn(),
|
||||
openCommandMenu: vi.fn(),
|
||||
openMediaRequestSearch: vi.fn(),
|
||||
refreshCurrentView: vi.fn().mockRejectedValue(new Error("offline")),
|
||||
});
|
||||
|
||||
await expect(executors.refresh_current_view()).resolves.toEqual({
|
||||
success: false,
|
||||
error: "The current Homarr view could not be refreshed.",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ interface AssistantBrowserToolDependencies {
|
||||
navigate: (path: string) => void;
|
||||
openCommandMenu: () => void;
|
||||
openMediaRequestSearch: () => void;
|
||||
refreshCurrentView: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const resolveAssistantInternalRoute = (path: string, origin: string) => {
|
||||
@@ -19,6 +20,7 @@ export const createAssistantBrowserToolExecutors = ({
|
||||
navigate,
|
||||
openCommandMenu,
|
||||
openMediaRequestSearch,
|
||||
refreshCurrentView,
|
||||
}: AssistantBrowserToolDependencies) => ({
|
||||
navigate_to_route: async ({ path }: { path: string }) => {
|
||||
const internalPath = resolveAssistantInternalRoute(path, getOrigin());
|
||||
@@ -37,6 +39,14 @@ export const createAssistantBrowserToolExecutors = ({
|
||||
openMediaRequestSearch();
|
||||
return { success: true as const };
|
||||
},
|
||||
refresh_current_view: async () => {
|
||||
try {
|
||||
await refreshCurrentView();
|
||||
return { success: true as const };
|
||||
} catch {
|
||||
return { success: false as const, error: "The current Homarr view could not be refreshed." };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export type AssistantBrowserToolExecutors = ReturnType<typeof createAssistantBrowserToolExecutors>;
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import type { ThreadMessage } from "@assistant-ui/react";
|
||||
|
||||
import {
|
||||
getSuccessfulApprovedAssistantMutationIds,
|
||||
updateAssistantMutationRefreshState,
|
||||
} from "./assistant-mutation-refresh";
|
||||
|
||||
const assistantMessage = (...content: ThreadMessage["content"]): ThreadMessage =>
|
||||
({
|
||||
id: "assistant-message",
|
||||
role: "assistant",
|
||||
content,
|
||||
status: { type: "complete", reason: "stop" },
|
||||
createdAt: new Date(),
|
||||
metadata: { custom: {} },
|
||||
}) as ThreadMessage;
|
||||
|
||||
describe("assistant mutation refresh", () => {
|
||||
test("returns only completed, approved mutation calls", () => {
|
||||
expect(
|
||||
getSuccessfulApprovedAssistantMutationIds([
|
||||
assistantMessage(
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "approved-1",
|
||||
toolName: "app_create",
|
||||
args: { name: "Wikipedia" },
|
||||
argsText: "{}",
|
||||
approval: { id: "approval-1", approved: true },
|
||||
result: { id: "app-1" },
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "read-1",
|
||||
toolName: "app_getAll",
|
||||
args: {},
|
||||
argsText: "{}",
|
||||
result: [],
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "pending-1",
|
||||
toolName: "board_savePartialBoardSettings",
|
||||
args: { id: "board-1" },
|
||||
argsText: "{}",
|
||||
approval: { id: "approval-2", approved: true },
|
||||
},
|
||||
),
|
||||
]),
|
||||
).toEqual(["approved-1"]);
|
||||
});
|
||||
|
||||
test("ignores denied and failed mutations", () => {
|
||||
expect(
|
||||
getSuccessfulApprovedAssistantMutationIds([
|
||||
assistantMessage(
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "denied-1",
|
||||
toolName: "app_create",
|
||||
args: {},
|
||||
argsText: "{}",
|
||||
approval: { id: "approval-1", approved: false },
|
||||
result: null,
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: "failed-1",
|
||||
toolName: "app_create",
|
||||
args: {},
|
||||
argsText: "{}",
|
||||
approval: { id: "approval-2", approved: true },
|
||||
result: { error: "Creation failed" },
|
||||
},
|
||||
),
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
test("baselines loaded history, refreshes once for new calls, and baselines a switched conversation", () => {
|
||||
const initial = updateAssistantMutationRefreshState({ conversationId: null, toolCallIds: new Set() }, "thread-1", [
|
||||
"historical-1",
|
||||
]);
|
||||
expect(initial.shouldRefresh).toBe(false);
|
||||
|
||||
const mutation = updateAssistantMutationRefreshState(initial.state, "thread-1", ["historical-1", "new-1", "new-2"]);
|
||||
expect(mutation.shouldRefresh).toBe(true);
|
||||
expect(updateAssistantMutationRefreshState(mutation.state, "thread-1", ["new-1", "new-2"]).shouldRefresh).toBe(
|
||||
false,
|
||||
);
|
||||
expect(updateAssistantMutationRefreshState(mutation.state, "thread-2", ["historical-2"]).shouldRefresh).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { ThreadMessage } from "@assistant-ui/react";
|
||||
|
||||
const resultHasError = (result: unknown) => typeof result === "object" && result !== null && "error" in result;
|
||||
|
||||
export const getSuccessfulApprovedAssistantMutationIds = (messages: readonly ThreadMessage[]) =>
|
||||
messages.flatMap((message) =>
|
||||
message.role === "assistant"
|
||||
? message.content.flatMap((part) =>
|
||||
part.type === "tool-call" &&
|
||||
part.approval?.approved === true &&
|
||||
part.result !== undefined &&
|
||||
part.isError !== true &&
|
||||
!resultHasError(part.result)
|
||||
? [part.toolCallId]
|
||||
: [],
|
||||
)
|
||||
: [],
|
||||
);
|
||||
|
||||
export interface AssistantMutationRefreshState {
|
||||
conversationId: string | null;
|
||||
toolCallIds: Set<string>;
|
||||
}
|
||||
|
||||
export const updateAssistantMutationRefreshState = (
|
||||
state: AssistantMutationRefreshState,
|
||||
conversationId: string,
|
||||
successfulMutationIds: readonly string[],
|
||||
) => {
|
||||
if (state.conversationId !== conversationId) {
|
||||
return {
|
||||
state: { conversationId, toolCallIds: new Set(successfulMutationIds) },
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
|
||||
const toolCallIds = new Set(state.toolCallIds);
|
||||
let shouldRefresh = false;
|
||||
for (const toolCallId of successfulMutationIds) {
|
||||
if (toolCallIds.has(toolCallId)) continue;
|
||||
toolCallIds.add(toolCallId);
|
||||
shouldRefresh = true;
|
||||
}
|
||||
|
||||
return { state: { conversationId, toolCallIds }, shouldRefresh };
|
||||
};
|
||||
@@ -97,6 +97,7 @@ import remarkGfm from "remark-gfm";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { assistantReasoningModes } from "@homarr/definitions";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import classes from "./assistant-panel.module.css";
|
||||
@@ -114,6 +115,8 @@ export interface AssistantConversationControls {
|
||||
models: AssistantRuntimeModelOption[];
|
||||
modelOptionsLoading: boolean;
|
||||
reasoning: AssistantReasoningMode;
|
||||
isRefreshing: boolean;
|
||||
onRefresh: () => Promise<void>;
|
||||
onModelChange: (modelId: string) => void;
|
||||
onReasoningChange: (reasoning: AssistantReasoningMode) => void;
|
||||
}
|
||||
@@ -1389,6 +1392,38 @@ const AutoApprovalControl = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ViewRefreshAction = ({
|
||||
isRefreshing,
|
||||
onRefresh,
|
||||
}: Pick<AssistantConversationControls, "isRefreshing" | "onRefresh">) => {
|
||||
const t = useScopedI18n("common.assistant.refresh");
|
||||
|
||||
const refresh = async () => {
|
||||
try {
|
||||
await onRefresh();
|
||||
showSuccessNotification({ title: t("completeTitle"), message: t("completeDescription") });
|
||||
} catch {
|
||||
showErrorNotification({ title: t("failedTitle"), message: t("failedDescription") });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={isRefreshing ? t("working") : t("action")}>
|
||||
<ActionIcon
|
||||
className={classes.panelAction}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
loading={isRefreshing}
|
||||
loaderProps={{ type: "bars" }}
|
||||
onClick={() => void refresh()}
|
||||
aria-label={isRefreshing ? t("working") : t("action")}
|
||||
>
|
||||
<IconRefresh size={17} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyThread = () => {
|
||||
const t = useScopedI18n("common.assistant");
|
||||
return (
|
||||
@@ -2015,6 +2050,8 @@ export const AssistantConversationSurface = ({
|
||||
models,
|
||||
modelOptionsLoading,
|
||||
reasoning,
|
||||
isRefreshing,
|
||||
onRefresh,
|
||||
onModelChange,
|
||||
onReasoningChange,
|
||||
onExpand,
|
||||
@@ -2041,6 +2078,7 @@ export const AssistantConversationSurface = ({
|
||||
</Group>
|
||||
<Group className={classes.panelActions} gap={2} wrap="nowrap">
|
||||
<ConversationHistory />
|
||||
<ViewRefreshAction isRefreshing={isRefreshing} onRefresh={onRefresh} />
|
||||
<AutoApprovalControl />
|
||||
<Tooltip label={t("newConversation")}>
|
||||
<ThreadListPrimitive.New asChild>
|
||||
@@ -2114,6 +2152,8 @@ export const AssistantConversationSurface = ({
|
||||
models={models}
|
||||
modelOptionsLoading={modelOptionsLoading}
|
||||
reasoning={reasoning}
|
||||
isRefreshing={isRefreshing}
|
||||
onRefresh={onRefresh}
|
||||
onModelChange={onModelChange}
|
||||
onReasoningChange={onReasoningChange}
|
||||
pendingAction={pendingAction}
|
||||
@@ -2140,6 +2180,8 @@ export const AssistantPanel = ({
|
||||
models,
|
||||
modelOptionsLoading,
|
||||
reasoning,
|
||||
isRefreshing,
|
||||
onRefresh,
|
||||
onModelChange,
|
||||
onReasoningChange,
|
||||
}: AssistantPanelProps) => {
|
||||
@@ -2192,6 +2234,8 @@ export const AssistantPanel = ({
|
||||
models={models}
|
||||
modelOptionsLoading={modelOptionsLoading}
|
||||
reasoning={reasoning}
|
||||
isRefreshing={isRefreshing}
|
||||
onRefresh={onRefresh}
|
||||
onModelChange={onModelChange}
|
||||
onReasoningChange={onReasoningChange}
|
||||
onMinimize={onClose}
|
||||
|
||||
@@ -39,7 +39,12 @@ import { createAssistantPromptInteraction } from "./assistant-spotlight";
|
||||
import { browserToolContracts } from "./assistant-tool-contracts";
|
||||
import { AssistantConfigureBoardSettingsTool } from "./assistant-board-settings-tool";
|
||||
import type { AssistantUIMessage } from "./assistant-message-metadata";
|
||||
import {
|
||||
getSuccessfulApprovedAssistantMutationIds,
|
||||
updateAssistantMutationRefreshState,
|
||||
} from "./assistant-mutation-refresh";
|
||||
import { initialAssistantNotificationState, updateAssistantNotificationState } from "./assistant-notifications";
|
||||
import { AssistantViewRefreshProvider, useAssistantViewRefresh } from "./assistant-view-refresh";
|
||||
import { AssistantBoardWidget } from "./assistant-widget";
|
||||
|
||||
interface AssistantContextValue {
|
||||
@@ -47,11 +52,13 @@ interface AssistantContextValue {
|
||||
unavailableDescription: string | null;
|
||||
opened: boolean;
|
||||
isRunning: boolean;
|
||||
isRefreshing: boolean;
|
||||
unreadCount: number;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
toggle: () => void;
|
||||
sendPrompt: (prompt: string) => void;
|
||||
refreshCurrentView: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AssistantContext = createContext<AssistantContextValue | null>(null);
|
||||
@@ -408,6 +415,7 @@ const AssistantThreadRuntime = () => {
|
||||
|
||||
const AssistantRuntime = ({ children }: PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
const { refreshCurrentView } = useAssistantViewRefresh();
|
||||
const runtime = useRemoteThreadListRuntime({
|
||||
adapter: threadAdapter,
|
||||
runtimeHook: AssistantThreadRuntime,
|
||||
@@ -419,8 +427,9 @@ const AssistantRuntime = ({ children }: PropsWithChildren) => {
|
||||
navigate: (path) => router.push(path),
|
||||
openCommandMenu: openSpotlight,
|
||||
openMediaRequestSearch,
|
||||
refreshCurrentView,
|
||||
}),
|
||||
[router],
|
||||
[refreshCurrentView, router],
|
||||
);
|
||||
|
||||
const toolkit = useMemo(
|
||||
@@ -465,6 +474,12 @@ const AssistantRuntime = ({ children }: PropsWithChildren) => {
|
||||
execute: browserToolExecutors.open_media_request_search,
|
||||
renderText: { running: "Opening media search…", complete: "Media search opened" },
|
||||
},
|
||||
refresh_current_view: {
|
||||
type: "frontend",
|
||||
...browserToolContracts.refresh_current_view,
|
||||
execute: browserToolExecutors.refresh_current_view,
|
||||
renderText: { running: "Refreshing current view…", complete: "Current view refreshed" },
|
||||
},
|
||||
}) as Toolkit,
|
||||
[browserToolExecutors],
|
||||
);
|
||||
@@ -541,6 +556,7 @@ const EnabledAssistantProvider = ({ children }: PropsWithChildren) => {
|
||||
const [queuedPrompt, setQueuedPrompt] = useState<string | null>(null);
|
||||
const aui = useAui();
|
||||
const preferences = useAssistantPreferences();
|
||||
const { isRefreshing, refreshCurrentView } = useAssistantViewRefresh();
|
||||
const messages = useAuiState((state) => state.thread.messages);
|
||||
// The local id is stable while a new thread is initialized and changes for every real thread switch.
|
||||
const conversationId = useAuiState((state) => state.threadListItem.id);
|
||||
@@ -555,6 +571,11 @@ const EnabledAssistantProvider = ({ children }: PropsWithChildren) => {
|
||||
const assistantIsRunning = isRunning || queuedPrompt !== null;
|
||||
const notificationKey = getNotificationKey(latestAssistantMessage);
|
||||
const notificationStateRef = useRef(initialAssistantNotificationState);
|
||||
const mutationRefreshStateRef = useRef<{ conversationId: string | null; toolCallIds: Set<string> }>({
|
||||
conversationId: null,
|
||||
toolCallIds: new Set(),
|
||||
});
|
||||
const successfulMutationIds = useMemo(() => getSuccessfulApprovedAssistantMutationIds(messages), [messages]);
|
||||
|
||||
const markRead = useCallback(() => {
|
||||
notificationStateRef.current = {
|
||||
@@ -633,6 +654,25 @@ const EnabledAssistantProvider = ({ children }: PropsWithChildren) => {
|
||||
setUnreadCount((current) => current + 1);
|
||||
}, [conversationId, isLoading, notificationKey, opened]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
const update = updateAssistantMutationRefreshState(
|
||||
mutationRefreshStateRef.current,
|
||||
conversationId,
|
||||
successfulMutationIds,
|
||||
);
|
||||
mutationRefreshStateRef.current = update.state;
|
||||
if (!update.shouldRefresh) return;
|
||||
|
||||
void refreshCurrentView().catch(() => {
|
||||
showWarningNotification({
|
||||
title: t("refresh.failedTitle"),
|
||||
message: t("refresh.failedDescription"),
|
||||
});
|
||||
});
|
||||
}, [conversationId, isLoading, refreshCurrentView, successfulMutationIds, t]);
|
||||
|
||||
useHotkeys([[hotkeys.openAssistant, open, { preventDefault: true }]]);
|
||||
|
||||
const spotlightItem = useMemo(
|
||||
@@ -654,13 +694,15 @@ const EnabledAssistantProvider = ({ children }: PropsWithChildren) => {
|
||||
unavailableDescription: null,
|
||||
opened,
|
||||
isRunning: assistantIsRunning,
|
||||
isRefreshing,
|
||||
unreadCount,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
sendPrompt,
|
||||
refreshCurrentView,
|
||||
}),
|
||||
[assistantIsRunning, close, open, opened, sendPrompt, toggle, unreadCount],
|
||||
[assistantIsRunning, close, isRefreshing, open, opened, refreshCurrentView, sendPrompt, toggle, unreadCount],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -686,6 +728,8 @@ const EnabledAssistantProvider = ({ children }: PropsWithChildren) => {
|
||||
models={preferences.models}
|
||||
modelOptionsLoading={preferences.isLoading}
|
||||
reasoning={preferences.reasoning}
|
||||
isRefreshing={isRefreshing}
|
||||
onRefresh={refreshCurrentView}
|
||||
onModelChange={selectModel}
|
||||
onReasoningChange={preferences.setReasoning}
|
||||
/>
|
||||
@@ -720,11 +764,13 @@ const DisabledAssistantProvider = ({ children, description }: DisabledAssistantP
|
||||
unavailableDescription: description,
|
||||
opened: false,
|
||||
isRunning: false,
|
||||
isRefreshing: false,
|
||||
unreadCount: 0,
|
||||
open: () => undefined,
|
||||
close: () => undefined,
|
||||
toggle: () => undefined,
|
||||
sendPrompt: () => undefined,
|
||||
refreshCurrentView: () => Promise.resolve(),
|
||||
}),
|
||||
[description],
|
||||
);
|
||||
@@ -748,9 +794,11 @@ export const AssistantProvider = ({ children }: PropsWithChildren) => {
|
||||
if (enabled) {
|
||||
return (
|
||||
<AssistantPreferencesProvider>
|
||||
<AssistantRuntime>
|
||||
<EnabledAssistantProvider>{children}</EnabledAssistantProvider>
|
||||
</AssistantRuntime>
|
||||
<AssistantViewRefreshProvider>
|
||||
<AssistantRuntime>
|
||||
<EnabledAssistantProvider>{children}</EnabledAssistantProvider>
|
||||
</AssistantRuntime>
|
||||
</AssistantViewRefreshProvider>
|
||||
</AssistantPreferencesProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const supportedToolArguments = {
|
||||
navigate_to_route: { path: "/manage/apps" },
|
||||
open_command_menu: {},
|
||||
open_media_request_search: {},
|
||||
refresh_current_view: {},
|
||||
} satisfies Record<keyof typeof browserToolContracts, unknown>;
|
||||
|
||||
describe("assistant human tool contracts", () => {
|
||||
|
||||
@@ -100,6 +100,11 @@ export const browserToolContracts = {
|
||||
description: "Open Homarr's media request search interface.",
|
||||
parameters: z.object({}),
|
||||
},
|
||||
refresh_current_view: {
|
||||
description:
|
||||
"Refetch the active Homarr page and its visible dashboard data. Use when the user asks to reload, or when the current view still needs to reflect an already completed change. Never use this before a mutation is approved and completed.",
|
||||
parameters: z.object({}),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type AskUserArgs = z.infer<(typeof browserToolContracts)["ask_user"]["parameters"]>;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface AssistantViewRefreshContextValue {
|
||||
isRefreshing: boolean;
|
||||
refreshCurrentView: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AssistantViewRefreshContext = createContext<AssistantViewRefreshContextValue | null>(null);
|
||||
|
||||
export const AssistantViewRefreshProvider = ({ children }: PropsWithChildren) => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const refreshPromiseRef = useRef<Promise<void> | null>(null);
|
||||
const refreshAgainRef = useRef(false);
|
||||
|
||||
const refreshCurrentView = useCallback(() => {
|
||||
if (refreshPromiseRef.current) {
|
||||
refreshAgainRef.current = true;
|
||||
return refreshPromiseRef.current;
|
||||
}
|
||||
|
||||
setIsRefreshing(true);
|
||||
const refreshPromise = (async () => {
|
||||
do {
|
||||
refreshAgainRef.current = false;
|
||||
router.refresh();
|
||||
await queryClient.invalidateQueries({ refetchType: "active" });
|
||||
} while (refreshAgainRef.current);
|
||||
})().finally(() => {
|
||||
refreshPromiseRef.current = null;
|
||||
setIsRefreshing(false);
|
||||
});
|
||||
refreshPromiseRef.current = refreshPromise;
|
||||
return refreshPromise;
|
||||
}, [queryClient, router]);
|
||||
|
||||
const value = useMemo(() => ({ isRefreshing, refreshCurrentView }), [isRefreshing, refreshCurrentView]);
|
||||
|
||||
return <AssistantViewRefreshContext.Provider value={value}>{children}</AssistantViewRefreshContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAssistantViewRefresh = () => {
|
||||
const value = useContext(AssistantViewRefreshContext);
|
||||
if (!value) throw new Error("useAssistantViewRefresh must be used within AssistantViewRefreshProvider");
|
||||
return value;
|
||||
};
|
||||
@@ -116,6 +116,8 @@ const EnabledAssistantBoardWidget = ({ options, width, height, isEditMode }: Wid
|
||||
models={preferences.models}
|
||||
modelOptionsLoading={preferences.isLoading}
|
||||
reasoning={preferences.reasoning}
|
||||
isRefreshing={assistant.isRefreshing}
|
||||
onRefresh={assistant.refreshCurrentView}
|
||||
onModelChange={selectModel}
|
||||
onReasoningChange={preferences.setReasoning}
|
||||
onExpand={assistant.open}
|
||||
|
||||
Reference in new issue
Block a user