Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fff5a9f90 | |||
| 66a2fb73b7 | |||
| be80dbe2db | |||
| b4b9cccbc2 | |||
| 13a31c96c2 | |||
| 70892c38e4 |
@@ -239,7 +239,6 @@ describe("archiveChat optimistic update", () => {
|
||||
const initialChats = [makeChat(chatId)];
|
||||
seedInfiniteChats(queryClient, initialChats);
|
||||
queryClient.setQueryData(chatKey(chatId), makeChat(chatId));
|
||||
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
|
||||
|
||||
const mutation = archiveChat(queryClient);
|
||||
const context = await mutation.onMutate(chatId);
|
||||
@@ -247,13 +246,12 @@ describe("archiveChat optimistic update", () => {
|
||||
// Verify the optimistic update took effect.
|
||||
expect(readInfiniteChats(queryClient)?.[0].archived).toBe(true);
|
||||
|
||||
// Simulate an error — the onError handler invalidates the
|
||||
// cache so a re-fetch restores the correct state.
|
||||
// Simulate an error — the onError handler restores the
|
||||
// previous cache state synchronously.
|
||||
mutation.onError(new Error("server error"), chatId, context);
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ queryKey: chatsKey }),
|
||||
);
|
||||
// The infinite cache should be restored to the pre-mutation state.
|
||||
expect(readInfiniteChats(queryClient)?.[0].archived).toBe(false);
|
||||
});
|
||||
|
||||
it("rolls back the individual chat cache on error", async () => {
|
||||
@@ -279,7 +277,6 @@ describe("archiveChat optimistic update", () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
const chatId = "chat-1";
|
||||
seedInfiniteChats(queryClient, [makeChat(chatId, { archived: true })]);
|
||||
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
|
||||
|
||||
const mutation = archiveChat(queryClient);
|
||||
|
||||
@@ -287,11 +284,6 @@ describe("archiveChat optimistic update", () => {
|
||||
expect(() => {
|
||||
mutation.onError(new Error("fail"), chatId, undefined);
|
||||
}).not.toThrow();
|
||||
|
||||
// The handler should still invalidate to trigger a refetch.
|
||||
expect(invalidateSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ queryKey: chatsKey }),
|
||||
);
|
||||
});
|
||||
|
||||
it("handles onMutate when no individual chat cache exists", async () => {
|
||||
@@ -364,7 +356,6 @@ describe("unarchiveChat optimistic update", () => {
|
||||
chatKey(chatId),
|
||||
makeChat(chatId, { archived: true }),
|
||||
);
|
||||
const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
|
||||
|
||||
const mutation = unarchiveChat(queryClient);
|
||||
const context = await mutation.onMutate(chatId);
|
||||
@@ -378,10 +369,8 @@ describe("unarchiveChat optimistic update", () => {
|
||||
// Roll back.
|
||||
mutation.onError(new Error("server error"), chatId, context);
|
||||
|
||||
// The chats list is rolled back via invalidation.
|
||||
expect(invalidateSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ queryKey: chatsKey }),
|
||||
);
|
||||
// The infinite cache is restored synchronously.
|
||||
expect(readInfiniteChats(queryClient)?.[0].archived).toBe(true);
|
||||
// The individual chat cache is restored directly.
|
||||
expect(
|
||||
queryClient.getQueryData<TypesGen.Chat>(chatKey(chatId))?.archived,
|
||||
@@ -694,16 +683,16 @@ describe("infiniteChats", () => {
|
||||
const lastPage = Array.from({ length: PAGE_LIMIT - 1 }, (_, i) =>
|
||||
makeChat(`chat-${i}`),
|
||||
);
|
||||
expect(getNextPageParam(lastPage, [lastPage])).toBeUndefined();
|
||||
expect(getNextPageParam(lastPage, [lastPage], 0)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns pages.length + 1 when lastPage has exactly the limit", () => {
|
||||
it("returns lastPageParam + limit when lastPage has exactly the limit", () => {
|
||||
const { getNextPageParam } = infiniteChats();
|
||||
const lastPage = Array.from({ length: PAGE_LIMIT }, (_, i) =>
|
||||
makeChat(`chat-${i}`),
|
||||
);
|
||||
const pages = [lastPage];
|
||||
expect(getNextPageParam(lastPage, pages)).toBe(pages.length + 1);
|
||||
expect(getNextPageParam(lastPage, pages, 0)).toBe(PAGE_LIMIT);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -718,39 +707,22 @@ describe("infiniteChats", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("computes offset 0 for pageParam <= 0", async () => {
|
||||
vi.mocked(API.getChats).mockResolvedValue([]);
|
||||
const { queryFn } = infiniteChats();
|
||||
await queryFn({ pageParam: -1 });
|
||||
expect(API.getChats).toHaveBeenCalledWith({
|
||||
limit: PAGE_LIMIT,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("computes correct offset for subsequent pages", async () => {
|
||||
vi.mocked(API.getChats).mockResolvedValue([]);
|
||||
const { queryFn } = infiniteChats();
|
||||
|
||||
await queryFn({ pageParam: 2 });
|
||||
await queryFn({ pageParam: PAGE_LIMIT });
|
||||
expect(API.getChats).toHaveBeenCalledWith({
|
||||
limit: PAGE_LIMIT,
|
||||
offset: PAGE_LIMIT,
|
||||
});
|
||||
|
||||
await queryFn({ pageParam: 3 });
|
||||
await queryFn({ pageParam: PAGE_LIMIT * 2 });
|
||||
expect(API.getChats).toHaveBeenCalledWith({
|
||||
limit: PAGE_LIMIT,
|
||||
offset: PAGE_LIMIT * 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws when pageParam is not a number", () => {
|
||||
const { queryFn } = infiniteChats();
|
||||
expect(() => queryFn({ pageParam: "bad" })).toThrow(
|
||||
"pageParam must be a number",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { API } from "api/api";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import type { QueryClient, UseInfiniteQueryOptions } from "react-query";
|
||||
import type { QueryClient } from "react-query";
|
||||
|
||||
export const chatsKey = ["chats"] as const;
|
||||
export const chatKey = (chatId: string) => ["chats", chatId] as const;
|
||||
@@ -20,7 +20,7 @@ export const updateInfiniteChatsCache = (
|
||||
queryClient.setQueriesData<{
|
||||
pages: TypesGen.Chat[][];
|
||||
pageParams: unknown[];
|
||||
}>({ queryKey: chatsKey, predicate: isChatListQuery }, (prev) => {
|
||||
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery }, (prev) => {
|
||||
if (!prev) return prev;
|
||||
if (!prev.pages) return prev;
|
||||
const nextPages = prev.pages.map((page) => updater(page));
|
||||
@@ -44,7 +44,7 @@ export const prependToInfiniteChatsCache = (
|
||||
queryClient.setQueriesData<{
|
||||
pages: TypesGen.Chat[][];
|
||||
pageParams: unknown[];
|
||||
}>({ queryKey: chatsKey, predicate: isChatListQuery }, (prev) => {
|
||||
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery }, (prev) => {
|
||||
if (!prev?.pages) return prev;
|
||||
// Check across ALL pages to avoid duplicates.
|
||||
const exists = prev.pages.some((page) =>
|
||||
@@ -69,7 +69,7 @@ export const readInfiniteChatsCache = (
|
||||
const queries = queryClient.getQueriesData<{
|
||||
pages: TypesGen.Chat[][];
|
||||
pageParams: unknown[];
|
||||
}>({ queryKey: chatsKey, predicate: isChatListQuery });
|
||||
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery });
|
||||
for (const [, data] of queries) {
|
||||
if (data?.pages) {
|
||||
return data.pages.flat();
|
||||
@@ -79,24 +79,46 @@ export const readInfiniteChatsCache = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate only the sidebar chat-list queries (flat + infinite)
|
||||
/**
|
||||
* Predicate that matches only chat-list queries (the sidebar), not
|
||||
* per-chat queries (detail, messages, diffs, cost).
|
||||
* Predicate that matches only infinite chat-list queries, excluding
|
||||
* the flat ["chats"] query. Used for direct cache manipulation
|
||||
* (setQueriesData) where the data structure must be { pages,
|
||||
* pageParams }.
|
||||
*
|
||||
* Sidebar keys look like ["chats"] or ["chats", <object|undefined>].
|
||||
* Infinite keys look like ["chats", <object | undefined>].
|
||||
* Per-chat keys look like ["chats", <string-id>, ...].
|
||||
*/
|
||||
const isInfiniteChatListQuery = (query: {
|
||||
queryKey: readonly unknown[];
|
||||
}): boolean => {
|
||||
const key = query.queryKey;
|
||||
// Exclude: ["chats"] (flat list) — its data is Chat[], not
|
||||
// { pages, pageParams }, so cache helpers must not touch it.
|
||||
if (key.length <= 1) return false;
|
||||
// Match: ["chats", <object | undefined>] (infinite query
|
||||
// with optional filter opts like {archived, q}).
|
||||
const segment = key[1];
|
||||
return (
|
||||
segment === undefined || (segment !== null && typeof segment === "object")
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Predicate that matches both flat and infinite chat-list queries
|
||||
* (the sidebar). Used for invalidation where we want to refresh
|
||||
* all list views.
|
||||
*/
|
||||
const isChatListQuery = (query: { queryKey: readonly unknown[] }): boolean => {
|
||||
const key = query.queryKey;
|
||||
// Match: ["chats"] (flat list).
|
||||
if (key.length <= 1) return true;
|
||||
// Match: ["chats", <object | undefined>] (infinite query
|
||||
// with optional filter opts like {archived, q}).
|
||||
const segment = key[1];
|
||||
return segment === undefined || typeof segment === "object";
|
||||
// Delegate to the infinite predicate for longer keys.
|
||||
return isInfiniteChatListQuery(query);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate only the sidebar chat-list queries (flat + infinite).
|
||||
*/
|
||||
|
||||
export const invalidateChatListQueries = (queryClient: QueryClient) => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: chatsKey,
|
||||
@@ -121,25 +143,26 @@ export const infiniteChats = (opts?: { q?: string; archived?: boolean }) => {
|
||||
|
||||
return {
|
||||
queryKey: [...chatsKey, opts],
|
||||
getNextPageParam: (lastPage: TypesGen.Chat[], pages: TypesGen.Chat[][]) => {
|
||||
getNextPageParam: (
|
||||
lastPage: TypesGen.Chat[],
|
||||
_allPages: TypesGen.Chat[][],
|
||||
lastPageParam: number,
|
||||
) => {
|
||||
if (lastPage.length < limit) {
|
||||
return undefined;
|
||||
}
|
||||
return pages.length + 1;
|
||||
return lastPageParam + limit;
|
||||
},
|
||||
initialPageParam: 0,
|
||||
queryFn: ({ pageParam }: { pageParam: unknown }) => {
|
||||
if (typeof pageParam !== "number") {
|
||||
throw new Error("pageParam must be a number");
|
||||
}
|
||||
queryFn: ({ pageParam }: { pageParam: number }) => {
|
||||
return API.getChats({
|
||||
limit,
|
||||
offset: pageParam <= 0 ? 0 : (pageParam - 1) * limit,
|
||||
offset: pageParam,
|
||||
q,
|
||||
});
|
||||
},
|
||||
refetchOnWindowFocus: true as const,
|
||||
} satisfies UseInfiniteQueryOptions<TypesGen.Chat[]>;
|
||||
};
|
||||
};
|
||||
|
||||
export const chats = () => ({
|
||||
@@ -179,12 +202,7 @@ export const archiveChat = (queryClient: QueryClient) => ({
|
||||
onMutate: async (chatId: string) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: chatsKey,
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
if (key.length <= 1) return true;
|
||||
const segment = key[1];
|
||||
return segment === undefined || typeof segment === "object";
|
||||
},
|
||||
predicate: isChatListQuery,
|
||||
});
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: chatKey(chatId),
|
||||
@@ -193,6 +211,12 @@ export const archiveChat = (queryClient: QueryClient) => ({
|
||||
const previousChat = queryClient.getQueryData<TypesGen.Chat>(
|
||||
chatKey(chatId),
|
||||
);
|
||||
// Snapshot infinite cache before optimistic update so we
|
||||
// can restore it on error without a flash of stale data.
|
||||
const previousInfiniteData = queryClient.getQueriesData<{
|
||||
pages: TypesGen.Chat[][];
|
||||
pageParams: unknown[];
|
||||
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery });
|
||||
updateInfiniteChatsCache(queryClient, (chats) =>
|
||||
chats.map((chat) =>
|
||||
chat.id === chatId ? { ...chat, archived: true } : chat,
|
||||
@@ -204,7 +228,7 @@ export const archiveChat = (queryClient: QueryClient) => ({
|
||||
archived: true,
|
||||
});
|
||||
}
|
||||
return { previousChat };
|
||||
return { previousChat, previousInfiniteData };
|
||||
},
|
||||
onError: (
|
||||
_error: unknown,
|
||||
@@ -212,11 +236,18 @@ export const archiveChat = (queryClient: QueryClient) => ({
|
||||
context:
|
||||
| {
|
||||
previousChat?: TypesGen.Chat;
|
||||
previousInfiniteData?: [
|
||||
readonly unknown[],
|
||||
{ pages: TypesGen.Chat[][]; pageParams: unknown[] } | undefined,
|
||||
][];
|
||||
}
|
||||
| undefined,
|
||||
) => {
|
||||
// Rollback: invalidate to re-fetch the correct state.
|
||||
void invalidateChatListQueries(queryClient);
|
||||
// Rollback: restore both caches synchronously. The
|
||||
// onSettled handler will refetch for server consistency.
|
||||
for (const [key, data] of context?.previousInfiniteData ?? []) {
|
||||
queryClient.setQueryData(key, data);
|
||||
}
|
||||
if (context?.previousChat) {
|
||||
queryClient.setQueryData<TypesGen.Chat>(
|
||||
chatKey(chatId),
|
||||
@@ -238,12 +269,7 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
|
||||
onMutate: async (chatId: string) => {
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: chatsKey,
|
||||
predicate: (query) => {
|
||||
const key = query.queryKey;
|
||||
if (key.length <= 1) return true;
|
||||
const segment = key[1];
|
||||
return segment === undefined || typeof segment === "object";
|
||||
},
|
||||
predicate: isChatListQuery,
|
||||
});
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: chatKey(chatId),
|
||||
@@ -252,6 +278,12 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
|
||||
const previousChat = queryClient.getQueryData<TypesGen.Chat>(
|
||||
chatKey(chatId),
|
||||
);
|
||||
// Snapshot infinite cache before optimistic update so we
|
||||
// can restore it on error without a flash of stale data.
|
||||
const previousInfiniteData = queryClient.getQueriesData<{
|
||||
pages: TypesGen.Chat[][];
|
||||
pageParams: unknown[];
|
||||
}>({ queryKey: chatsKey, predicate: isInfiniteChatListQuery });
|
||||
updateInfiniteChatsCache(queryClient, (chats) =>
|
||||
chats.map((chat) =>
|
||||
chat.id === chatId ? { ...chat, archived: false } : chat,
|
||||
@@ -263,7 +295,7 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
|
||||
archived: false,
|
||||
});
|
||||
}
|
||||
return { previousChat };
|
||||
return { previousChat, previousInfiniteData };
|
||||
},
|
||||
onError: (
|
||||
_error: unknown,
|
||||
@@ -271,11 +303,18 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
|
||||
context:
|
||||
| {
|
||||
previousChat?: TypesGen.Chat;
|
||||
previousInfiniteData?: [
|
||||
readonly unknown[],
|
||||
{ pages: TypesGen.Chat[][]; pageParams: unknown[] } | undefined,
|
||||
][];
|
||||
}
|
||||
| undefined,
|
||||
) => {
|
||||
// Rollback: invalidate to re-fetch the correct state.
|
||||
void invalidateChatListQueries(queryClient);
|
||||
// Rollback: restore both caches synchronously. The
|
||||
// onSettled handler will refetch for server consistency.
|
||||
for (const [key, data] of context?.previousInfiniteData ?? []) {
|
||||
queryClient.setQueryData(key, data);
|
||||
}
|
||||
if (context?.previousChat) {
|
||||
queryClient.setQueryData<TypesGen.Chat>(
|
||||
chatKey(chatId),
|
||||
@@ -294,8 +333,8 @@ export const unarchiveChat = (queryClient: QueryClient) => ({
|
||||
|
||||
export const createChat = (queryClient: QueryClient) => ({
|
||||
mutationFn: (req: TypesGen.CreateChatRequest) => API.createChat(req),
|
||||
onSuccess: () => {
|
||||
void invalidateChatListQueries(queryClient);
|
||||
onSuccess: async () => {
|
||||
await invalidateChatListQueries(queryClient);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -318,17 +357,17 @@ type EditChatMessageMutationArgs = {
|
||||
export const editChatMessage = (queryClient: QueryClient, chatId: string) => ({
|
||||
mutationFn: ({ messageId, req }: EditChatMessageMutationArgs) =>
|
||||
API.editChatMessage(chatId, messageId, req),
|
||||
onSuccess: () => {
|
||||
onSuccess: async () => {
|
||||
// Editing truncates all messages after the edited one on the
|
||||
// server. The WebSocket can insert/update messages but cannot
|
||||
// remove stale ones, so a full messages refetch is required.
|
||||
// Use exact matching to avoid cascading to unrelated queries
|
||||
// (diff-status, diff-contents, cost summaries, etc.).
|
||||
void queryClient.invalidateQueries({
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: chatKey(chatId),
|
||||
exact: true,
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: chatMessagesKey(chatId),
|
||||
exact: true,
|
||||
});
|
||||
|
||||
@@ -675,7 +675,7 @@ const AgentDetail: FC = () => {
|
||||
if (!agentId || interruptMutation.isPending) {
|
||||
return;
|
||||
}
|
||||
void interruptMutation.mutateAsync();
|
||||
interruptMutation.mutate();
|
||||
};
|
||||
|
||||
const handleDeleteQueuedMessage = useCallback(
|
||||
|
||||
@@ -563,8 +563,19 @@ export const useChatStore = (
|
||||
if (hasStaleEntries) {
|
||||
store.replaceMessages(chatMessages);
|
||||
} else {
|
||||
let anyChanged = false;
|
||||
for (const message of chatMessages) {
|
||||
store.upsertDurableMessage(message);
|
||||
const { changed } = store.upsertDurableMessage(message);
|
||||
if (changed) {
|
||||
anyChanged = true;
|
||||
}
|
||||
}
|
||||
// If a REST refetch delivered a new or updated message
|
||||
// while streaming is still active, clear stream state
|
||||
// to avoid showing the same content in both the durable
|
||||
// message list and the streaming output.
|
||||
if (anyChanged && storeSnap.streamState !== null) {
|
||||
store.clearStreamState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,17 +155,23 @@ const AgentsPage: FC = () => {
|
||||
await API.deleteWorkspace(workspaceId);
|
||||
return { chatId, workspaceId };
|
||||
},
|
||||
onSuccess: async ({ chatId }) => {
|
||||
onSuccess: ({ chatId }) => {
|
||||
clearChatErrorReason(chatId);
|
||||
await invalidateChatListQueries(queryClient);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: chatKey(chatId),
|
||||
exact: true,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error, "Failed to archive agent."));
|
||||
},
|
||||
onSettled: async (_data, _error, variables) => {
|
||||
// Always invalidate — even on partial failure the chat
|
||||
// may have been archived server-side.
|
||||
if (variables) {
|
||||
await invalidateChatListQueries(queryClient);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: chatKey(variables.chatId),
|
||||
exact: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
const unarchiveChatBase = unarchiveChat(queryClient);
|
||||
const unarchiveAgentMutation = useMutation({
|
||||
@@ -503,7 +509,11 @@ const AgentsPage: FC = () => {
|
||||
modelCatalogError={chatModelsQuery.error}
|
||||
isAgentsAdmin={isAgentsAdmin}
|
||||
hasNextPage={chatsQuery.hasNextPage}
|
||||
onLoadMore={() => void chatsQuery.fetchNextPage()}
|
||||
onLoadMore={() => {
|
||||
if (!chatsQuery.isFetching) {
|
||||
void chatsQuery.fetchNextPage();
|
||||
}
|
||||
}}
|
||||
isFetchingNextPage={chatsQuery.isFetchingNextPage}
|
||||
archivedFilter={archivedFilter}
|
||||
onArchivedFilterChange={setArchivedFilter}
|
||||
|
||||
@@ -352,6 +352,12 @@ export const ChatModelAdminPanel: FC<ChatModelAdminPanelProps> = ({
|
||||
})
|
||||
}
|
||||
onDeleteModel={(id) => deleteModelMut.mutateAsync(id)}
|
||||
onSetDefaultModel={(modelConfigId) =>
|
||||
updateModelMut.mutate({
|
||||
modelConfigId,
|
||||
req: { is_default: true },
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ interface ModelFormProps {
|
||||
req: TypesGen.UpdateChatModelConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
onCancel: () => void;
|
||||
onDeleteModel?: (modelConfigId: string) => Promise<void>;
|
||||
onDeleteModel?: (modelConfigId: string) => void;
|
||||
}
|
||||
|
||||
export const ModelForm: FC<ModelFormProps> = ({
|
||||
@@ -528,7 +528,7 @@ export const ModelForm: FC<ModelFormProps> = ({
|
||||
size="lg"
|
||||
type="button"
|
||||
disabled={isDeleting}
|
||||
onClick={() => void onDeleteModel(editingModel.id)}
|
||||
onClick={() => onDeleteModel(editingModel.id)}
|
||||
>
|
||||
{isDeleting && <Spinner className="h-4 w-4" loading />}
|
||||
Delete model
|
||||
|
||||
@@ -52,7 +52,8 @@ interface ModelsSectionProps {
|
||||
modelConfigId: string,
|
||||
req: TypesGen.UpdateChatModelConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
onDeleteModel: (modelConfigId: string) => Promise<void>;
|
||||
onDeleteModel: (modelConfigId: string) => Promise<unknown>;
|
||||
onSetDefaultModel: (modelConfigId: string) => void;
|
||||
}
|
||||
|
||||
export const ModelsSection: FC<ModelsSectionProps> = ({
|
||||
@@ -71,6 +72,7 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
|
||||
onCreateModel,
|
||||
onUpdateModel,
|
||||
onDeleteModel,
|
||||
onSetDefaultModel,
|
||||
}) => {
|
||||
const [view, setView] = useState<ModelView>({ mode: "list" });
|
||||
|
||||
@@ -116,8 +118,13 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
|
||||
onDeleteModel={
|
||||
editingModel
|
||||
? async (id) => {
|
||||
await onDeleteModel(id);
|
||||
setView({ mode: "list" });
|
||||
try {
|
||||
await onDeleteModel(id);
|
||||
setView({ mode: "list" });
|
||||
} catch {
|
||||
// Error is surfaced via mutation state
|
||||
// in the parent.
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
@@ -161,7 +168,7 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
|
||||
|
||||
const handleSetDefault = (modelConfig: TypesGen.ChatModelConfig) => {
|
||||
if (modelConfig.is_default) return;
|
||||
void onUpdateModel(modelConfig.id, { is_default: true });
|
||||
onSetDefaultModel(modelConfig.id);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,7 @@ interface ProviderFormProps {
|
||||
providerConfigId: string,
|
||||
req: TypesGen.UpdateChatProviderConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
onDeleteProvider: (providerConfigId: string) => Promise<void>;
|
||||
onDeleteProvider: (providerConfigId: string) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
size="lg"
|
||||
type="button"
|
||||
disabled={isProviderMutationPending}
|
||||
onClick={() => void onDeleteProvider(providerConfig.id)}
|
||||
onClick={() => onDeleteProvider(providerConfig.id)}
|
||||
>
|
||||
{isProviderMutationPending && (
|
||||
<Spinner className="h-4 w-4" loading />
|
||||
|
||||
@@ -23,7 +23,7 @@ interface ProvidersSectionProps {
|
||||
providerConfigId: string,
|
||||
req: TypesGen.UpdateChatProviderConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
onDeleteProvider: (providerConfigId: string) => Promise<void>;
|
||||
onDeleteProvider: (providerConfigId: string) => Promise<unknown>;
|
||||
onSelectedProviderChange: (provider: string) => void;
|
||||
}
|
||||
|
||||
@@ -61,8 +61,12 @@ export const ProvidersSection: FC<ProvidersSectionProps> = ({
|
||||
onCreateProvider={onCreateProvider}
|
||||
onUpdateProvider={onUpdateProvider}
|
||||
onDeleteProvider={async (id) => {
|
||||
await onDeleteProvider(id);
|
||||
setView({ mode: "list" });
|
||||
try {
|
||||
await onDeleteProvider(id);
|
||||
setView({ mode: "list" });
|
||||
} catch {
|
||||
// Error is surfaced via mutation state in the parent.
|
||||
}
|
||||
}}
|
||||
onBack={() => setView({ mode: "list" })}
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useQuery } from "react-query";
|
||||
import { type PRInsightsTimeRange, PRInsightsView } from "./PRInsightsView";
|
||||
|
||||
function timeRangeToDates(range: PRInsightsTimeRange) {
|
||||
const end = dayjs();
|
||||
const end = dayjs().startOf("minute");
|
||||
const days = Number.parseInt(range, 10);
|
||||
const start = end.subtract(days, "day");
|
||||
return {
|
||||
|
||||
@@ -148,7 +148,7 @@ const UsageContent: FC<UsageContentProps> = ({ now }) => {
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
const summaryQuery = useQuery({
|
||||
...chatCostSummary(selectedUser?.user_id ?? "me", {
|
||||
...chatCostSummary(selectedUser?.user_id ?? "", {
|
||||
start_date: dateRange.startDate,
|
||||
end_date: dateRange.endDate,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user