feat(21-05): create chat API client and TanStack Query hooks
- Add ui/src/api/chat.ts with chatApi (7 methods: list/create/get/update/delete conversations + list/post messages) - Add ui/src/hooks/useChatConversations.ts with useInfiniteQuery + placeholderData + CRUD mutations - Add ui/src/hooks/useChatMessages.ts with useInfiniteQuery + sendMutation + flattened/reversed messages array - Fix [Rule 3 - Blocking]: export ChatConversation, ChatMessage, ChatConversationListResponse, ChatMessageListResponse from packages/shared/src/index.ts (types existed in types/chat.ts but were not re-exported at package root)
This commit is contained in:
parent
de721ffc05
commit
c268f2d03e
4 changed files with 145 additions and 0 deletions
|
|
@ -597,6 +597,14 @@ export {
|
|||
type CreateMessage,
|
||||
} from "./validators/index.js";
|
||||
|
||||
export type {
|
||||
ChatConversation,
|
||||
ChatConversationListItem,
|
||||
ChatMessage,
|
||||
ChatConversationListResponse,
|
||||
ChatMessageListResponse,
|
||||
} from "./types/chat.js";
|
||||
|
||||
export { API_PREFIX, API } from "./api.js";
|
||||
export { normalizeAgentUrlKey, deriveAgentUrlKey, isUuidLike } from "./agent-url-key.js";
|
||||
export { deriveProjectUrlKey, normalizeProjectUrlKey, hasNonAsciiContent } from "./project-url-key.js";
|
||||
|
|
|
|||
55
ui/src/api/chat.ts
Normal file
55
ui/src/api/chat.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { api } from "./client";
|
||||
import type {
|
||||
ChatConversation,
|
||||
ChatConversationListResponse,
|
||||
ChatMessage,
|
||||
ChatMessageListResponse,
|
||||
} from "@paperclipai/shared";
|
||||
|
||||
export const chatApi = {
|
||||
listConversations(companyId: string, opts?: { cursor?: string; limit?: number }) {
|
||||
const params = new URLSearchParams();
|
||||
if (opts?.cursor) params.set("cursor", opts.cursor);
|
||||
if (opts?.limit) params.set("limit", String(opts.limit));
|
||||
const qs = params.toString();
|
||||
return api.get<ChatConversationListResponse>(
|
||||
`/companies/${companyId}/conversations${qs ? `?${qs}` : ""}`,
|
||||
);
|
||||
},
|
||||
|
||||
createConversation(companyId: string, data?: { title?: string; agentId?: string }) {
|
||||
return api.post<ChatConversation>(`/companies/${companyId}/conversations`, data ?? {});
|
||||
},
|
||||
|
||||
getConversation(id: string) {
|
||||
return api.get<ChatConversation>(`/conversations/${id}`);
|
||||
},
|
||||
|
||||
updateConversation(
|
||||
id: string,
|
||||
data: { title?: string; pinnedAt?: string | null; archivedAt?: string | null },
|
||||
) {
|
||||
return api.patch<ChatConversation>(`/conversations/${id}`, data);
|
||||
},
|
||||
|
||||
deleteConversation(id: string) {
|
||||
return api.delete<void>(`/conversations/${id}`);
|
||||
},
|
||||
|
||||
listMessages(conversationId: string, opts?: { cursor?: string; limit?: number }) {
|
||||
const params = new URLSearchParams();
|
||||
if (opts?.cursor) params.set("cursor", opts.cursor);
|
||||
if (opts?.limit) params.set("limit", String(opts.limit));
|
||||
const qs = params.toString();
|
||||
return api.get<ChatMessageListResponse>(
|
||||
`/conversations/${conversationId}/messages${qs ? `?${qs}` : ""}`,
|
||||
);
|
||||
},
|
||||
|
||||
postMessage(
|
||||
conversationId: string,
|
||||
data: { role: string; content: string; agentId?: string },
|
||||
) {
|
||||
return api.post<ChatMessage>(`/conversations/${conversationId}/messages`, data);
|
||||
},
|
||||
};
|
||||
49
ui/src/hooks/useChatConversations.ts
Normal file
49
ui/src/hooks/useChatConversations.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { chatApi } from "../api/chat";
|
||||
import type { ChatConversationListResponse } from "@paperclipai/shared";
|
||||
|
||||
export function useChatConversations(companyId: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["chat", "conversations", companyId],
|
||||
queryFn: ({ pageParam }) =>
|
||||
chatApi.listConversations(companyId!, { cursor: pageParam as string | undefined }),
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage: ChatConversationListResponse) =>
|
||||
lastPage.hasMore ? lastPage.items.at(-1)?.updatedAt : undefined,
|
||||
enabled: !!companyId,
|
||||
placeholderData: (prev) => prev, // keepPreviousData equivalent -- prevents flicker (Pitfall 6)
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data?: { title?: string }) => chatApi.createConversation(companyId!, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", "conversations", companyId] });
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
...data
|
||||
}: {
|
||||
id: string;
|
||||
title?: string;
|
||||
pinnedAt?: string | null;
|
||||
archivedAt?: string | null;
|
||||
}) => chatApi.updateConversation(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", "conversations", companyId] });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => chatApi.deleteConversation(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", "conversations", companyId] });
|
||||
},
|
||||
});
|
||||
|
||||
return { ...query, createMutation, updateMutation, deleteMutation };
|
||||
}
|
||||
33
ui/src/hooks/useChatMessages.ts
Normal file
33
ui/src/hooks/useChatMessages.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useInfiniteQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { chatApi } from "../api/chat";
|
||||
import type { ChatMessage, ChatMessageListResponse } from "@paperclipai/shared";
|
||||
|
||||
export function useChatMessages(conversationId: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const query = useInfiniteQuery({
|
||||
queryKey: ["chat", "messages", conversationId],
|
||||
queryFn: ({ pageParam }) =>
|
||||
chatApi.listMessages(conversationId!, { cursor: pageParam as string | undefined }),
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage: ChatMessageListResponse) =>
|
||||
lastPage.hasMore ? lastPage.items.at(-1)?.createdAt : undefined,
|
||||
enabled: !!conversationId,
|
||||
});
|
||||
|
||||
const sendMutation = useMutation({
|
||||
mutationFn: (data: { content: string }) =>
|
||||
chatApi.postMessage(conversationId!, { role: "user", content: data.content }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", "messages", conversationId] });
|
||||
// Also invalidate conversations to update lastMessagePreview and sort order
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", "conversations"] });
|
||||
},
|
||||
});
|
||||
|
||||
// Flatten pages into a single sorted array (oldest first for display)
|
||||
// Messages come from API in desc(createdAt) order -- reversing gives chronological order
|
||||
const messages: ChatMessage[] = query.data?.pages.flatMap((p) => p.items).reverse() ?? [];
|
||||
|
||||
return { ...query, messages, sendMutation };
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue