From d56e19c7b42841cc38533a0757a89c665ccaafe8 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Wed, 1 Apr 2026 22:31:05 +0000 Subject: [PATCH] feat(24-02): API client methods and React Query hooks for search, bookmarks, branches - Add searchMessages, toggleBookmark, getBookmarks, branchConversation, listBranches, exportConversation to chatApi - Create useChatSearch hook with debounced FTS, placeholderData, 30s staleTime - Create useChatBookmarks and useToggleBookmark with cache invalidation for bookmarks and search queries --- ui/src/api/chat.ts | 45 ++++++++++++++++++++++++++++++++ ui/src/hooks/useChatBookmarks.ts | 33 +++++++++++++++++++++++ ui/src/hooks/useChatSearch.ts | 12 +++++++++ 3 files changed, 90 insertions(+) create mode 100644 ui/src/hooks/useChatBookmarks.ts create mode 100644 ui/src/hooks/useChatSearch.ts diff --git a/ui/src/api/chat.ts b/ui/src/api/chat.ts index 70883c07..1206134e 100644 --- a/ui/src/api/chat.ts +++ b/ui/src/api/chat.ts @@ -4,6 +4,9 @@ import type { ChatConversationListResponse, ChatMessage, ChatMessageListResponse, + ChatMessageSearchResponse, + ChatBookmarkToggleResponse, + ChatBookmarkListResponse, } from "@paperclipai/shared"; export const chatApi = { @@ -166,4 +169,46 @@ export const chatApi = { ) { return api.post<{ id: string }>(`/conversations/${conversationId}/status-update`, data); }, + + searchMessages(companyId: string, q: string, limit?: number) { + const params = new URLSearchParams({ q }); + if (limit) params.set("limit", String(limit)); + return api.get( + `/companies/${companyId}/messages/search?${params}`, + ); + }, + + toggleBookmark(conversationId: string, messageId: string) { + return api.post( + `/conversations/${conversationId}/bookmarks`, + { messageId }, + ); + }, + + getBookmarks(companyId: string, conversationId?: string) { + const params = new URLSearchParams(); + if (conversationId) params.set("conversationId", conversationId); + const qs = params.toString(); + return api.get( + `/companies/${companyId}/bookmarks${qs ? `?${qs}` : ""}`, + ); + }, + + branchConversation(conversationId: string, branchFromMessageId: string) { + return api.post( + `/conversations/${conversationId}/branch`, + { branchFromMessageId }, + ); + }, + + listBranches(conversationId: string) { + return api.get<{ items: ChatConversation[] }>( + `/conversations/${conversationId}/branches`, + ); + }, + + exportConversation(conversationId: string, format: "markdown" | "json") { + // Returns a download URL — use window.location.href to trigger + return `/api/conversations/${conversationId}/export?format=${format}`; + }, }; diff --git a/ui/src/hooks/useChatBookmarks.ts b/ui/src/hooks/useChatBookmarks.ts new file mode 100644 index 00000000..17454e07 --- /dev/null +++ b/ui/src/hooks/useChatBookmarks.ts @@ -0,0 +1,33 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { chatApi } from "../api/chat"; + +export function useChatBookmarks(companyId: string | null, conversationId?: string) { + const query = useQuery({ + queryKey: ["chat", "bookmarks", companyId, conversationId], + queryFn: () => chatApi.getBookmarks(companyId!, conversationId), + enabled: !!companyId, + }); + + return { + data: query.data, + isLoading: query.isLoading, + }; +} + +export function useToggleBookmark() { + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: ({ conversationId, messageId }: { conversationId: string; messageId: string }) => + chatApi.toggleBookmark(conversationId, messageId), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ["chat", "bookmarks"] }); + void queryClient.invalidateQueries({ queryKey: ["chat", "search"] }); + }, + }); + + return { + toggleBookmark: mutation.mutate, + isPending: mutation.isPending, + }; +} diff --git a/ui/src/hooks/useChatSearch.ts b/ui/src/hooks/useChatSearch.ts new file mode 100644 index 00000000..4b922e13 --- /dev/null +++ b/ui/src/hooks/useChatSearch.ts @@ -0,0 +1,12 @@ +import { useQuery } from "@tanstack/react-query"; +import { chatApi } from "../api/chat"; + +export function useChatSearch(companyId: string | null, query: string) { + return useQuery({ + queryKey: ["chat", "search", companyId, query], + queryFn: () => chatApi.searchMessages(companyId!, query), + enabled: !!companyId && query.trim().length >= 2, + placeholderData: (prev) => prev, + staleTime: 30_000, + }); +}