Merge upstream/dev

This commit is contained in:
CREDO23 2026-03-31 20:21:12 +02:00
commit 440762fb07
92 changed files with 3227 additions and 2502 deletions

View file

@ -2,6 +2,8 @@ import { atomWithMutation } from "jotai-tanstack-query";
import { toast } from "sonner";
import type {
CreateImageGenConfigRequest,
CreateImageGenConfigResponse,
DeleteImageGenConfigResponse,
GetImageGenConfigsResponse,
UpdateImageGenConfigRequest,
UpdateImageGenConfigResponse,
@ -23,14 +25,14 @@ export const createImageGenConfigMutationAtom = atomWithMutation((get) => {
mutationFn: async (request: CreateImageGenConfigRequest) => {
return imageGenConfigApiService.createConfig(request);
},
onSuccess: () => {
toast.success("Image model configuration created");
onSuccess: (_: CreateImageGenConfigResponse, request: CreateImageGenConfigRequest) => {
toast.success(`${request.name} created`);
queryClient.invalidateQueries({
queryKey: cacheKeys.imageGenConfigs.all(Number(searchSpaceId)),
});
},
onError: (error: Error) => {
toast.error(error.message || "Failed to create image model configuration");
toast.error(error.message || "Failed to create image model");
},
};
});
@ -48,7 +50,7 @@ export const updateImageGenConfigMutationAtom = atomWithMutation((get) => {
return imageGenConfigApiService.updateConfig(request);
},
onSuccess: (_: UpdateImageGenConfigResponse, request: UpdateImageGenConfigRequest) => {
toast.success("Image model configuration updated");
toast.success(`${request.data.name ?? "Configuration"} updated`);
queryClient.invalidateQueries({
queryKey: cacheKeys.imageGenConfigs.all(Number(searchSpaceId)),
});
@ -57,7 +59,7 @@ export const updateImageGenConfigMutationAtom = atomWithMutation((get) => {
});
},
onError: (error: Error) => {
toast.error(error.message || "Failed to update image model configuration");
toast.error(error.message || "Failed to update image model");
},
};
});
@ -71,21 +73,21 @@ export const deleteImageGenConfigMutationAtom = atomWithMutation((get) => {
return {
mutationKey: ["image-gen-configs", "delete"],
enabled: !!searchSpaceId,
mutationFn: async (id: number) => {
return imageGenConfigApiService.deleteConfig(id);
mutationFn: async (request: { id: number; name: string }) => {
return imageGenConfigApiService.deleteConfig(request.id);
},
onSuccess: (_, id: number) => {
toast.success("Image model configuration deleted");
onSuccess: (_: DeleteImageGenConfigResponse, request: { id: number; name: string }) => {
toast.success(`${request.name} deleted`);
queryClient.setQueryData(
cacheKeys.imageGenConfigs.all(Number(searchSpaceId)),
(oldData: GetImageGenConfigsResponse | undefined) => {
if (!oldData) return oldData;
return oldData.filter((config) => config.id !== id);
return oldData.filter((config) => config.id !== request.id);
}
);
},
onError: (error: Error) => {
toast.error(error.message || "Failed to delete image model configuration");
toast.error(error.message || "Failed to delete image model");
},
};
});

View file

@ -2,7 +2,9 @@ import { atomWithMutation } from "jotai-tanstack-query";
import { toast } from "sonner";
import type {
CreateNewLLMConfigRequest,
CreateNewLLMConfigResponse,
DeleteNewLLMConfigRequest,
DeleteNewLLMConfigResponse,
GetNewLLMConfigsResponse,
UpdateLLMPreferencesRequest,
UpdateNewLLMConfigRequest,
@ -25,14 +27,14 @@ export const createNewLLMConfigMutationAtom = atomWithMutation((get) => {
mutationFn: async (request: CreateNewLLMConfigRequest) => {
return newLLMConfigApiService.createConfig(request);
},
onSuccess: () => {
toast.success("Configuration created successfully");
onSuccess: (_: CreateNewLLMConfigResponse, request: CreateNewLLMConfigRequest) => {
toast.success(`${request.name} created`);
queryClient.invalidateQueries({
queryKey: cacheKeys.newLLMConfigs.all(Number(searchSpaceId)),
});
},
onError: (error: Error) => {
toast.error(error.message || "Failed to create configuration");
toast.error(error.message || "Failed to create LLM model");
},
};
});
@ -50,7 +52,7 @@ export const updateNewLLMConfigMutationAtom = atomWithMutation((get) => {
return newLLMConfigApiService.updateConfig(request);
},
onSuccess: (_: UpdateNewLLMConfigResponse, request: UpdateNewLLMConfigRequest) => {
toast.success("Configuration updated successfully");
toast.success(`${request.data.name ?? "Configuration"} updated`);
queryClient.invalidateQueries({
queryKey: cacheKeys.newLLMConfigs.all(Number(searchSpaceId)),
});
@ -59,7 +61,7 @@ export const updateNewLLMConfigMutationAtom = atomWithMutation((get) => {
});
},
onError: (error: Error) => {
toast.error(error.message || "Failed to update configuration");
toast.error(error.message || "Failed to update");
},
};
});
@ -73,11 +75,14 @@ export const deleteNewLLMConfigMutationAtom = atomWithMutation((get) => {
return {
mutationKey: ["new-llm-configs", "delete"],
enabled: !!searchSpaceId,
mutationFn: async (request: DeleteNewLLMConfigRequest) => {
return newLLMConfigApiService.deleteConfig(request);
mutationFn: async (request: DeleteNewLLMConfigRequest & { name: string }) => {
return newLLMConfigApiService.deleteConfig({ id: request.id });
},
onSuccess: (_, request: DeleteNewLLMConfigRequest) => {
toast.success("Configuration deleted successfully");
onSuccess: (
_: DeleteNewLLMConfigResponse,
request: DeleteNewLLMConfigRequest & { name: string }
) => {
toast.success(`${request.name} deleted`);
queryClient.setQueryData(
cacheKeys.newLLMConfigs.all(Number(searchSpaceId)),
(oldData: GetNewLLMConfigsResponse | undefined) => {
@ -87,7 +92,7 @@ export const deleteNewLLMConfigMutationAtom = atomWithMutation((get) => {
);
},
onError: (error: Error) => {
toast.error(error.message || "Failed to delete configuration");
toast.error(error.message || "Failed to delete");
},
};
});

View file

@ -33,6 +33,9 @@ const initialState: TabsState = {
activeTabId: "chat-new",
};
// Prevent race conditions where route-sync recreates a just-deleted chat tab.
const deletedChatIdsAtom = atom<Set<number>>(new Set<number>());
const sessionStorageAdapter = createJSONStorage<TabsState>(
() => (typeof window !== "undefined" ? sessionStorage : undefined) as Storage
);
@ -71,6 +74,10 @@ export const syncChatTabAtom = atom(
set,
{ chatId, title, chatUrl }: { chatId: number | null; title?: string; chatUrl?: string }
) => {
if (chatId && get(deletedChatIdsAtom).has(chatId)) {
return;
}
const state = get(tabsStateAtom);
const tabId = makeChatTabId(chatId);
const existing = state.tabs.find((t) => t.id === tabId);
@ -128,6 +135,19 @@ export const updateChatTabTitleAtom = atom(
(get, set, { chatId, title }: { chatId: number; title: string }) => {
const state = get(tabsStateAtom);
const tabId = makeChatTabId(chatId);
const hasExactTab = state.tabs.some((t) => t.id === tabId);
// During lazy thread creation, title updates can arrive before "chat-new"
// is swapped to chat-{id}. In that case, promote the active "chat-new" tab.
if (!hasExactTab && state.activeTabId === "chat-new") {
set(tabsStateAtom, {
...state,
activeTabId: tabId,
tabs: state.tabs.map((t) => (t.id === "chat-new" ? { ...t, id: tabId, chatId, title } : t)),
});
return;
}
set(tabsStateAtom, {
...state,
tabs: state.tabs.map((t) => (t.id === tabId ? { ...t, title } : t)),
@ -213,7 +233,39 @@ export const closeTabAtom = atom(null, (get, set, tabId: string) => {
return remaining.find((t) => t.id === newActiveId) ?? null;
});
/** Remove a chat tab by chat ID (used when a chat is deleted). */
export const removeChatTabAtom = atom(null, (get, set, chatId: number) => {
const state = get(tabsStateAtom);
const tabId = makeChatTabId(chatId);
const idx = state.tabs.findIndex((t) => t.id === tabId);
if (idx === -1) return null;
const deletedChatIds = get(deletedChatIdsAtom);
set(deletedChatIdsAtom, new Set([...deletedChatIds, chatId]));
const remaining = state.tabs.filter((t) => t.id !== tabId);
// Always keep at least one tab available.
if (remaining.length === 0) {
set(tabsStateAtom, {
tabs: [INITIAL_CHAT_TAB],
activeTabId: "chat-new",
});
return INITIAL_CHAT_TAB;
}
let newActiveId = state.activeTabId;
if (state.activeTabId === tabId) {
const newIdx = Math.min(idx, remaining.length - 1);
newActiveId = remaining[newIdx].id;
}
set(tabsStateAtom, { tabs: remaining, activeTabId: newActiveId });
return remaining.find((t) => t.id === newActiveId) ?? null;
});
/** Reset tabs when switching search spaces. */
export const resetTabsAtom = atom(null, (_get, set) => {
set(tabsStateAtom, { ...initialState });
set(deletedChatIdsAtom, new Set<number>());
});