feat: implement chat persistence with MongoDB
- Add full chat persistence to database (conversations and messages) - Implement MongoChatRepository with cursor-based pagination - Add getConversations/getConversation endpoints in ChatController - Save user and assistant messages in ChatService.processMessage() - Track respondedAction (confirm/reject) on proposed event messages - Load existing messages on chat screen mount - Add addMessages() bulk action and chatMessageToMessageData() helper to ChatStore - Add RespondedAction type and UpdateMessageDTO to shared types
This commit is contained in:
@@ -1,18 +1,25 @@
|
||||
import { View, Text, TextInput, Pressable, KeyboardAvoidingView, Platform, Keyboard } from "react-native";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
Pressable,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Keyboard,
|
||||
} from "react-native";
|
||||
import currentTheme from "../../Themes";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import Header from "../../components/Header";
|
||||
import BaseBackground from "../../components/BaseBackground";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import { ChatService } from "../../services";
|
||||
import { useChatStore, MessageData } from "../../stores";
|
||||
import { useChatStore, chatMessageToMessageData, MessageData } from "../../stores";
|
||||
import { ProposedEventChange } from "@caldav/shared";
|
||||
import { ProposedEventCard } from "../../components/ProposedEventCard";
|
||||
|
||||
// TODO: better shadows for everything
|
||||
// (maybe with extra library because of differences between android and ios)
|
||||
// TODO: max width for messages
|
||||
// TODO: create new messages
|
||||
|
||||
type BubbleSide = "left" | "right";
|
||||
|
||||
@@ -30,14 +37,43 @@ type ChatInputProps = {
|
||||
};
|
||||
|
||||
const Chat = () => {
|
||||
const { messages, addMessage, updateMessage } = useChatStore();
|
||||
const listRef = useRef<FlashList<MessageData>>(null);
|
||||
const { messages, addMessage, addMessages, updateMessage } = useChatStore();
|
||||
const listRef =
|
||||
useRef<React.ComponentRef<typeof FlashList<MessageData>>>(null);
|
||||
const [currentConversationId, setCurrentConversationId] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
useEffect(() => {
|
||||
const keyboardDidShow = Keyboard.addListener("keyboardDidShow", scrollToEnd);
|
||||
const keyboardDidShow = Keyboard.addListener(
|
||||
"keyboardDidShow",
|
||||
scrollToEnd,
|
||||
);
|
||||
return () => keyboardDidShow.remove();
|
||||
}, []);
|
||||
|
||||
// Load existing messages from database on mount
|
||||
useEffect(() => {
|
||||
const fetchMessages = async () => {
|
||||
try {
|
||||
const conversationSummaries = await ChatService.getConversations();
|
||||
if (conversationSummaries.length > 0) {
|
||||
const conversationId = conversationSummaries[0].id;
|
||||
setCurrentConversationId(conversationId);
|
||||
|
||||
const serverMessages =
|
||||
await ChatService.getConversation(conversationId);
|
||||
const clientMessages = serverMessages.map(chatMessageToMessageData);
|
||||
addMessages(clientMessages);
|
||||
scrollToEnd();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load messages:", error);
|
||||
}
|
||||
};
|
||||
fetchMessages();
|
||||
}, []);
|
||||
|
||||
const scrollToEnd = () => {
|
||||
setTimeout(() => {
|
||||
listRef.current?.scrollToEnd({ animated: true });
|
||||
@@ -87,13 +123,22 @@ const Chat = () => {
|
||||
id: Date.now().toString(),
|
||||
side: "right",
|
||||
content: text,
|
||||
conversationId: currentConversationId,
|
||||
};
|
||||
addMessage(userMessage);
|
||||
scrollToEnd();
|
||||
|
||||
try {
|
||||
// Fetch server response
|
||||
const response = await ChatService.sendMessage({ content: text });
|
||||
// Fetch server response (include conversationId for existing conversations)
|
||||
const response = await ChatService.sendMessage({
|
||||
content: text,
|
||||
conversationId: currentConversationId,
|
||||
});
|
||||
|
||||
// Track conversation ID for subsequent messages
|
||||
if (!currentConversationId) {
|
||||
setCurrentConversationId(response.conversationId);
|
||||
}
|
||||
|
||||
// Show bot response
|
||||
const botMessage: MessageData = {
|
||||
|
||||
@@ -47,13 +47,20 @@ export const ChatService = {
|
||||
},
|
||||
|
||||
getConversations: async (): Promise<ConversationSummary[]> => {
|
||||
throw new Error("Not implemented");
|
||||
return ApiClient.get<ConversationSummary[]>("/chat/conversations");
|
||||
},
|
||||
|
||||
getConversation: async (
|
||||
_id: string,
|
||||
_options?: GetMessagesOptions,
|
||||
id: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]> => {
|
||||
throw new Error("Not implemented");
|
||||
const params = new URLSearchParams();
|
||||
if (options?.before) params.append("before", options.before);
|
||||
if (options?.limit) params.append("limit", options.limit.toString());
|
||||
|
||||
const queryString = params.toString();
|
||||
const url = `/chat/conversations/${id}${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
return ApiClient.get<ChatMessage[]>(url);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import { ProposedEventChange } from "@caldav/shared";
|
||||
import { ChatMessage, ProposedEventChange } from "@caldav/shared";
|
||||
|
||||
type BubbleSide = "left" | "right";
|
||||
|
||||
@@ -14,6 +14,7 @@ export type MessageData = {
|
||||
|
||||
interface ChatState {
|
||||
messages: MessageData[];
|
||||
addMessages: (messages: MessageData[]) => void;
|
||||
addMessage: (message: MessageData) => void;
|
||||
updateMessage: (id: string, updates: Partial<MessageData>) => void;
|
||||
clearMessages: () => void;
|
||||
@@ -21,6 +22,9 @@ interface ChatState {
|
||||
|
||||
export const useChatStore = create<ChatState>((set) => ({
|
||||
messages: [],
|
||||
addMessages(messages) {
|
||||
set((state) => ({messages: [...state.messages, ...messages]}))
|
||||
},
|
||||
addMessage: (message: MessageData) => {
|
||||
set((state) => ({ messages: [...state.messages, message] }));
|
||||
},
|
||||
@@ -35,3 +39,15 @@ export const useChatStore = create<ChatState>((set) => ({
|
||||
set({ messages: [] });
|
||||
},
|
||||
}));
|
||||
|
||||
// Helper to convert server ChatMessage to client MessageData
|
||||
export function chatMessageToMessageData(msg: ChatMessage): MessageData {
|
||||
return {
|
||||
id: msg.id,
|
||||
side: msg.sender === "assistant" ? "left" : "right",
|
||||
content: msg.content,
|
||||
proposedChange: msg.proposedChange,
|
||||
respondedAction: msg.respondedAction,
|
||||
conversationId: msg.conversationId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { useAuthStore } from "./AuthStore";
|
||||
export { useChatStore, type MessageData } from "./ChatStore";
|
||||
export { useChatStore, chatMessageToMessageData, type MessageData } from "./ChatStore";
|
||||
export { useEventsStore } from "./EventsStore";
|
||||
|
||||
Reference in New Issue
Block a user