mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
feat(chat): Introduce centralized thread metadata management and update chat visibility handling with new hooks for thread mutations
This commit is contained in:
parent
0cfe5e52bd
commit
8b704b2fef
10 changed files with 832 additions and 105 deletions
249
surfsense_web/lib/chat/thread-cache.ts
Normal file
249
surfsense_web/lib/chat/thread-cache.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
import type { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||
import type {
|
||||
ThreadListItem,
|
||||
ThreadListResponse,
|
||||
ThreadRecord,
|
||||
} from "@/lib/chat/thread-persistence";
|
||||
|
||||
type SearchSpaceKey = number | string;
|
||||
|
||||
type ThreadMetadataPatch = Partial<ThreadRecord> &
|
||||
Partial<ThreadListItem> & {
|
||||
has_comments?: boolean;
|
||||
};
|
||||
|
||||
function isSameSearchSpace(keyValue: unknown, searchSpaceId: SearchSpaceKey): boolean {
|
||||
return String(keyValue) === String(searchSpaceId);
|
||||
}
|
||||
|
||||
function isThreadListResponse(value: unknown): value is ThreadListResponse {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
Array.isArray((value as ThreadListResponse).threads) &&
|
||||
Array.isArray((value as ThreadListResponse).archived_threads)
|
||||
);
|
||||
}
|
||||
|
||||
function isThreadListItemArray(value: unknown): value is ThreadListItem[] {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
function listItemPatchFromMetadata(patch: ThreadMetadataPatch): Partial<ThreadListItem> {
|
||||
const listPatch: Partial<ThreadListItem> = {};
|
||||
|
||||
if (patch.title !== undefined) listPatch.title = patch.title;
|
||||
if (patch.archived !== undefined) listPatch.archived = patch.archived;
|
||||
if (patch.visibility !== undefined) listPatch.visibility = patch.visibility;
|
||||
if (patch.created_by_id !== undefined) listPatch.created_by_id = patch.created_by_id;
|
||||
if (patch.created_at !== undefined) listPatch.createdAt = patch.created_at;
|
||||
if (patch.updated_at !== undefined) listPatch.updatedAt = patch.updated_at;
|
||||
if (patch.createdAt !== undefined) listPatch.createdAt = patch.createdAt;
|
||||
if (patch.updatedAt !== undefined) listPatch.updatedAt = patch.updatedAt;
|
||||
|
||||
return listPatch;
|
||||
}
|
||||
|
||||
function patchListItem(
|
||||
item: ThreadListItem,
|
||||
threadId: number,
|
||||
patch: ThreadMetadataPatch
|
||||
): ThreadListItem {
|
||||
if (item.id !== threadId) return item;
|
||||
return {
|
||||
...item,
|
||||
...listItemPatchFromMetadata(patch),
|
||||
};
|
||||
}
|
||||
|
||||
function patchThreadListResponse(
|
||||
response: ThreadListResponse,
|
||||
threadId: number,
|
||||
patch: ThreadMetadataPatch
|
||||
): ThreadListResponse {
|
||||
return {
|
||||
...response,
|
||||
threads: response.threads.map((item) => patchListItem(item, threadId, patch)),
|
||||
archived_threads: response.archived_threads.map((item) => patchListItem(item, threadId, patch)),
|
||||
};
|
||||
}
|
||||
|
||||
function patchThreadListItems(
|
||||
items: ThreadListItem[],
|
||||
threadId: number,
|
||||
patch: ThreadMetadataPatch
|
||||
): ThreadListItem[] {
|
||||
return items.map((item) => patchListItem(item, threadId, patch));
|
||||
}
|
||||
|
||||
function patchThreadRecord(
|
||||
record: ThreadRecord,
|
||||
threadId: number,
|
||||
patch: ThreadMetadataPatch
|
||||
): ThreadRecord {
|
||||
if (record.id !== threadId) return record;
|
||||
return {
|
||||
...record,
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
function threadListQueryFilter(searchSpaceId: SearchSpaceKey) {
|
||||
return {
|
||||
predicate: ({ queryKey }: { queryKey: QueryKey }) =>
|
||||
Array.isArray(queryKey) &&
|
||||
queryKey[0] === "threads" &&
|
||||
isSameSearchSpace(queryKey[1], searchSpaceId),
|
||||
};
|
||||
}
|
||||
|
||||
function allThreadsQueryFilter(searchSpaceId: SearchSpaceKey) {
|
||||
return {
|
||||
predicate: ({ queryKey }: { queryKey: QueryKey }) =>
|
||||
Array.isArray(queryKey) &&
|
||||
queryKey[0] === "all-threads" &&
|
||||
isSameSearchSpace(queryKey[1], searchSpaceId),
|
||||
};
|
||||
}
|
||||
|
||||
function searchThreadsQueryFilter(searchSpaceId: SearchSpaceKey) {
|
||||
return {
|
||||
predicate: ({ queryKey }: { queryKey: QueryKey }) =>
|
||||
Array.isArray(queryKey) &&
|
||||
queryKey[0] === "search-threads" &&
|
||||
isSameSearchSpace(queryKey[1], searchSpaceId),
|
||||
};
|
||||
}
|
||||
|
||||
function threadDetailQueryFilter(searchSpaceId: SearchSpaceKey, threadId: number) {
|
||||
return {
|
||||
predicate: ({ queryKey }: { queryKey: QueryKey }) =>
|
||||
Array.isArray(queryKey) &&
|
||||
((queryKey[0] === "threads" &&
|
||||
queryKey[1] === "detail" &&
|
||||
Number(queryKey[2]) === threadId) ||
|
||||
(queryKey[0] === "threads" &&
|
||||
isSameSearchSpace(queryKey[1], searchSpaceId) &&
|
||||
queryKey[2] === "detail" &&
|
||||
Number(queryKey[3]) === threadId)),
|
||||
};
|
||||
}
|
||||
|
||||
function updateThreadListResponse(
|
||||
queryClient: QueryClient,
|
||||
filter: ReturnType<typeof threadListQueryFilter>,
|
||||
threadId: number,
|
||||
patch: ThreadMetadataPatch
|
||||
): void {
|
||||
queryClient.setQueriesData<ThreadListResponse>(filter, (old) => {
|
||||
if (!isThreadListResponse(old)) return old;
|
||||
return patchThreadListResponse(old, threadId, patch);
|
||||
});
|
||||
}
|
||||
|
||||
export function patchThreadEverywhere(
|
||||
queryClient: QueryClient,
|
||||
searchSpaceId: SearchSpaceKey,
|
||||
threadId: number,
|
||||
patch: ThreadMetadataPatch
|
||||
): void {
|
||||
updateThreadListResponse(queryClient, threadListQueryFilter(searchSpaceId), threadId, patch);
|
||||
updateThreadListResponse(queryClient, allThreadsQueryFilter(searchSpaceId), threadId, patch);
|
||||
|
||||
queryClient.setQueriesData<ThreadListItem[]>(searchThreadsQueryFilter(searchSpaceId), (old) => {
|
||||
if (!isThreadListItemArray(old)) return old;
|
||||
return patchThreadListItems(old, threadId, patch);
|
||||
});
|
||||
|
||||
queryClient.setQueriesData<ThreadRecord>(
|
||||
threadDetailQueryFilter(searchSpaceId, threadId),
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
return patchThreadRecord(old, threadId, patch);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function replaceThreadEverywhere(
|
||||
queryClient: QueryClient,
|
||||
searchSpaceId: SearchSpaceKey,
|
||||
thread: ThreadRecord
|
||||
): void {
|
||||
patchThreadEverywhere(queryClient, searchSpaceId, thread.id, thread);
|
||||
}
|
||||
|
||||
export function removeThreadEverywhere(
|
||||
queryClient: QueryClient,
|
||||
searchSpaceId: SearchSpaceKey,
|
||||
threadId: number
|
||||
): void {
|
||||
const removeFromListResponse = (old: ThreadListResponse | undefined) => {
|
||||
if (!isThreadListResponse(old)) return old;
|
||||
return {
|
||||
...old,
|
||||
threads: old.threads.filter((thread) => thread.id !== threadId),
|
||||
archived_threads: old.archived_threads.filter((thread) => thread.id !== threadId),
|
||||
};
|
||||
};
|
||||
|
||||
queryClient.setQueriesData<ThreadListResponse>(
|
||||
threadListQueryFilter(searchSpaceId),
|
||||
removeFromListResponse
|
||||
);
|
||||
queryClient.setQueriesData<ThreadListResponse>(
|
||||
allThreadsQueryFilter(searchSpaceId),
|
||||
removeFromListResponse
|
||||
);
|
||||
queryClient.setQueriesData<ThreadListItem[]>(searchThreadsQueryFilter(searchSpaceId), (old) => {
|
||||
if (!isThreadListItemArray(old)) return old;
|
||||
return old.filter((thread) => thread.id !== threadId);
|
||||
});
|
||||
queryClient.removeQueries(threadDetailQueryFilter(searchSpaceId, threadId));
|
||||
}
|
||||
|
||||
export function moveThreadArchiveState(
|
||||
queryClient: QueryClient,
|
||||
searchSpaceId: SearchSpaceKey,
|
||||
threadId: number,
|
||||
archived: boolean
|
||||
): void {
|
||||
const moveInListResponse = (old: ThreadListResponse | undefined) => {
|
||||
if (!isThreadListResponse(old)) return old;
|
||||
|
||||
const activeWithoutThread = old.threads.filter((thread) => thread.id !== threadId);
|
||||
const archivedWithoutThread = old.archived_threads.filter((thread) => thread.id !== threadId);
|
||||
const existing =
|
||||
old.threads.find((thread) => thread.id === threadId) ??
|
||||
old.archived_threads.find((thread) => thread.id === threadId);
|
||||
|
||||
if (!existing) return old;
|
||||
|
||||
const updated = { ...existing, archived };
|
||||
|
||||
return {
|
||||
...old,
|
||||
threads: archived ? activeWithoutThread : [updated, ...activeWithoutThread],
|
||||
archived_threads: archived ? [updated, ...archivedWithoutThread] : archivedWithoutThread,
|
||||
};
|
||||
};
|
||||
|
||||
queryClient.setQueriesData<ThreadListResponse>(
|
||||
threadListQueryFilter(searchSpaceId),
|
||||
moveInListResponse
|
||||
);
|
||||
queryClient.setQueriesData<ThreadListResponse>(
|
||||
allThreadsQueryFilter(searchSpaceId),
|
||||
moveInListResponse
|
||||
);
|
||||
queryClient.setQueriesData<ThreadListItem[]>(searchThreadsQueryFilter(searchSpaceId), (old) => {
|
||||
if (!isThreadListItemArray(old)) return old;
|
||||
return old.map((thread) => (thread.id === threadId ? { ...thread, archived } : thread));
|
||||
});
|
||||
queryClient.setQueriesData<ThreadRecord>(
|
||||
threadDetailQueryFilter(searchSpaceId, threadId),
|
||||
(old) => {
|
||||
if (!old || old.id !== threadId) return old;
|
||||
return { ...old, archived };
|
||||
}
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue