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:
24
CLAUDE.md
24
CLAUDE.md
@@ -91,7 +91,7 @@ src/
|
||||
└── stores/ # Zustand state management
|
||||
├── index.ts # Re-exports all stores
|
||||
├── AuthStore.ts # user, token, isAuthenticated, login(), logout(), setToken()
|
||||
├── ChatStore.ts # messages[], addMessage(), updateMessage(), clearMessages()
|
||||
├── ChatStore.ts # messages[], addMessage(), addMessages(), updateMessage(), clearMessages(), chatMessageToMessageData()
|
||||
└── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent()
|
||||
```
|
||||
|
||||
@@ -172,7 +172,7 @@ src/
|
||||
│ ├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO, ExpandedEvent
|
||||
│ ├── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
|
||||
│ │ # GetMessagesOptions, ChatResponse, ConversationSummary,
|
||||
│ │ # ProposedEventChange, EventAction
|
||||
│ │ # ProposedEventChange, EventAction, RespondedAction, UpdateMessageDTO
|
||||
│ └── Constants.ts # DAYS, MONTHS, Day, Month, DAY_INDEX, DAY_INDEX_TO_DAY,
|
||||
│ # DAY_TO_GERMAN, DAY_TO_GERMAN_SHORT, MONTH_TO_GERMAN
|
||||
└── utils/
|
||||
@@ -184,12 +184,14 @@ src/
|
||||
- `User`: id, email, displayName, passwordHash?, createdAt?, updatedAt?
|
||||
- `CalendarEvent`: id, userId, title, description?, startTime, endTime, note?, isRecurring?, recurrenceRule?
|
||||
- `ExpandedEvent`: Extends CalendarEvent with occurrenceStart, occurrenceEnd (for recurring event instances)
|
||||
- `ChatMessage`: id, conversationId, sender ('user' | 'assistant'), content, proposedChange?
|
||||
- `ChatMessage`: id, conversationId, sender ('user' | 'assistant'), content, proposedChange?, respondedAction?
|
||||
- `ProposedEventChange`: action ('create' | 'update' | 'delete'), eventId?, event?, updates?
|
||||
- `Conversation`: id, userId, createdAt?, updatedAt? (messages loaded separately via lazy loading)
|
||||
- `CreateEventDTO`: Used for creating events AND for AI-proposed events
|
||||
- `GetMessagesOptions`: Cursor-based pagination with `before?: string` and `limit?: number`
|
||||
- `ConversationSummary`: id, lastMessage?, createdAt? (for conversation list)
|
||||
- `UpdateMessageDTO`: respondedAction? (for marking messages as confirmed/rejected)
|
||||
- `RespondedAction`: 'confirm' | 'reject' (tracks user response to proposed events)
|
||||
- `Day`: "Monday" | "Tuesday" | ... | "Sunday"
|
||||
- `Month`: "January" | "February" | ... | "December"
|
||||
|
||||
@@ -281,12 +283,14 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
||||
- `EventService`: Full CRUD with recurring event expansion via recurrenceExpander
|
||||
- `utils/eventFormatters`: getWeeksOverview(), getMonthOverview() with German localization
|
||||
- `utils/recurrenceExpander`: expandRecurringEvents() using rrule library for RRULE parsing
|
||||
- `ChatController`: getConversations(), getConversation() with cursor-based pagination support
|
||||
- `ChatService`: getConversations(), getConversation(), processMessage() now persists user/assistant messages to DB, confirmEvent()/rejectEvent() update respondedAction and persist response messages
|
||||
- `MongoChatRepository`: Full CRUD implemented (getConversationsByUser, createConversation, getMessages with cursor pagination, createMessage, updateMessage)
|
||||
- `ChatRepository` interface: updateMessage() added for respondedAction tracking
|
||||
- **Stubbed (TODO):**
|
||||
- `AuthMiddleware.authenticate()`: Currently uses fake user for testing
|
||||
- `AuthController`: refresh(), logout()
|
||||
- `AuthService`: refreshToken()
|
||||
- `ChatController`: getConversations(), getConversation()
|
||||
- `MongoChatRepository`: Database persistence for chat
|
||||
- **Not started:**
|
||||
- `ClaudeAdapter` (AI integration - currently using test responses)
|
||||
|
||||
@@ -302,20 +306,22 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
||||
- Tap-to-open modal overlay showing EventCards for selected day
|
||||
- Supports events from adjacent months visible in grid
|
||||
- Uses `useFocusEffect` for automatic reload on tab focus
|
||||
- Chat screen functional with FlashList, message sending, and event confirm/reject
|
||||
- Messages persisted via ChatStore (survives tab switches)
|
||||
- Chat screen fully functional with FlashList, message sending, and event confirm/reject
|
||||
- Messages persisted to database via ChatService and loaded on mount
|
||||
- Tracks conversationId for message continuity across sessions
|
||||
- ChatStore with addMessages() for bulk loading, chatMessageToMessageData() helper
|
||||
- KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height)
|
||||
- Auto-scroll to end on new messages and keyboard show
|
||||
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
||||
- `ApiClient`: get(), post(), put(), delete() implemented
|
||||
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully implemented
|
||||
- `ChatService`: sendMessage(), confirmEvent(convId, msgId, action, event?, eventId?, updates?), rejectEvent() - supports create/update/delete actions
|
||||
- `ChatService`: sendMessage(), confirmEvent(), rejectEvent(), getConversations(), getConversation() - fully implemented with cursor pagination
|
||||
- `EventCardBase`: Shared base component with event layout (header, date/time/recurring icons, description) - used by both EventCard and ProposedEventCard
|
||||
- `EventCard`: Uses EventCardBase + edit/delete buttons for calendar display
|
||||
- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions)
|
||||
- `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator, secondaryBg
|
||||
- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[]
|
||||
- `ChatStore`: Zustand store with addMessage(), updateMessage(), clearMessages() - persists messages across tab switches
|
||||
- `ChatStore`: Zustand store with addMessage(), addMessages(), updateMessage(), clearMessages() - loads from server on mount and persists across tab switches
|
||||
- Auth screens (Login, Register), Event Detail, and Note screens exist as skeletons
|
||||
- AuthStore defined with `throw new Error('Not implemented')`
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CreateEventDTO,
|
||||
UpdateEventDTO,
|
||||
EventAction,
|
||||
GetMessagesOptions,
|
||||
} from "@caldav/shared";
|
||||
import { ChatService } from "../services";
|
||||
import { AuthenticatedRequest } from "../middleware";
|
||||
@@ -66,13 +67,43 @@ export class ChatController {
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const conversations = await this.chatService.getConversations(userId);
|
||||
res.json(conversations);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: "Failed to get conversations" });
|
||||
}
|
||||
}
|
||||
|
||||
async getConversation(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
try {
|
||||
const userId = req.user!.userId;
|
||||
const { id } = req.params;
|
||||
const { before, limit } = req.query as {
|
||||
before?: string;
|
||||
limit?: string;
|
||||
};
|
||||
|
||||
const options: GetMessagesOptions = {};
|
||||
if (before) options.before = before;
|
||||
if (limit) options.limit = parseInt(limit, 10);
|
||||
|
||||
const messages = await this.chatService.getConversation(
|
||||
userId,
|
||||
id,
|
||||
options,
|
||||
);
|
||||
res.json(messages);
|
||||
} catch (error) {
|
||||
if ((error as Error).message === "Conversation not found") {
|
||||
res.status(404).json({ error: "Conversation not found" });
|
||||
} else {
|
||||
res.status(500).json({ error: "Failed to get conversation" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Conversation,
|
||||
CreateMessageDTO,
|
||||
GetMessagesOptions,
|
||||
UpdateMessageDTO,
|
||||
} from "@caldav/shared";
|
||||
import { ChatRepository } from "../../services/interfaces";
|
||||
import { ChatMessageModel, ConversationModel } from "./models";
|
||||
@@ -10,11 +11,15 @@ import { ChatMessageModel, ConversationModel } from "./models";
|
||||
export class MongoChatRepository implements ChatRepository {
|
||||
// Conversations
|
||||
async getConversationsByUser(userId: string): Promise<Conversation[]> {
|
||||
throw new Error("Not implemented");
|
||||
const conversations = await ConversationModel.find({ userId });
|
||||
return conversations.map((c) => c.toJSON() as unknown as Conversation);
|
||||
}
|
||||
|
||||
async createConversation(userId: string): Promise<Conversation> {
|
||||
throw new Error("Not implemented");
|
||||
const conversation = await ConversationModel.create({
|
||||
userId,
|
||||
});
|
||||
return conversation.toJSON() as unknown as Conversation;
|
||||
}
|
||||
|
||||
// Messages (cursor-based pagination)
|
||||
@@ -22,13 +27,44 @@ export class MongoChatRepository implements ChatRepository {
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]> {
|
||||
throw new Error("Not implemented");
|
||||
const limit = options?.limit ?? 20;
|
||||
const query: Record<string, unknown> = { conversationId };
|
||||
|
||||
// Cursor: load messages before this ID (for "load more" scrolling up)
|
||||
if (options?.before) {
|
||||
query._id = { $lt: options.before };
|
||||
}
|
||||
|
||||
// Fetch newest first, then reverse for chronological order
|
||||
const docs = await ChatMessageModel.find(query)
|
||||
.sort({ _id: -1 })
|
||||
.limit(limit);
|
||||
|
||||
return docs.reverse().map((doc) => doc.toJSON() as unknown as ChatMessage);
|
||||
}
|
||||
|
||||
async createMessage(
|
||||
conversationId: string,
|
||||
message: CreateMessageDTO,
|
||||
): Promise<ChatMessage> {
|
||||
throw new Error("Not implemented");
|
||||
const repoMessage = await ChatMessageModel.create({
|
||||
conversationId: conversationId,
|
||||
sender: message.sender,
|
||||
content: message.content,
|
||||
proposedChange: message.proposedChange,
|
||||
});
|
||||
return repoMessage.toJSON() as unknown as ChatMessage;
|
||||
}
|
||||
|
||||
async updateMessage(
|
||||
messageId: string,
|
||||
updates: UpdateMessageDTO,
|
||||
): Promise<ChatMessage | null> {
|
||||
const doc = await ChatMessageModel.findByIdAndUpdate(
|
||||
messageId,
|
||||
{ $set: updates },
|
||||
{ new: true },
|
||||
);
|
||||
return doc ? (doc.toJSON() as unknown as ChatMessage) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ const ChatMessageSchema = new Schema<
|
||||
proposedChange: {
|
||||
type: ProposedChangeSchema,
|
||||
},
|
||||
respondedAction: {
|
||||
type: String,
|
||||
enum: ["confirm", "reject"],
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CreateEventDTO,
|
||||
UpdateEventDTO,
|
||||
EventAction,
|
||||
CreateMessageDTO,
|
||||
} from "@caldav/shared";
|
||||
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
|
||||
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
|
||||
@@ -252,6 +253,18 @@ export class ChatService {
|
||||
userId: string,
|
||||
data: SendMessageDTO,
|
||||
): Promise<ChatResponse> {
|
||||
let conversationId = data.conversationId;
|
||||
if (!conversationId) {
|
||||
const conversation = await this.chatRepo.createConversation(userId);
|
||||
conversationId = conversation.id;
|
||||
}
|
||||
|
||||
// Save user message
|
||||
await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "user",
|
||||
content: data.content,
|
||||
});
|
||||
|
||||
const response = await getTestResponse(
|
||||
responseIndex,
|
||||
this.eventRepo,
|
||||
@@ -259,15 +272,14 @@ export class ChatService {
|
||||
);
|
||||
responseIndex++;
|
||||
|
||||
const message: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
conversationId: data.conversationId || "temp-conv-id",
|
||||
// Save and then return assistant response
|
||||
const answerMessage = await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "assistant",
|
||||
content: response.content,
|
||||
proposedChange: response.proposedChange,
|
||||
};
|
||||
});
|
||||
|
||||
return { message, conversationId: message.conversationId };
|
||||
return { message: answerMessage, conversationId: conversationId };
|
||||
}
|
||||
|
||||
async confirmEvent(
|
||||
@@ -279,6 +291,10 @@ export class ChatService {
|
||||
eventId?: string,
|
||||
updates?: UpdateEventDTO,
|
||||
): Promise<ChatResponse> {
|
||||
// Update original message with respondedAction
|
||||
await this.chatRepo.updateMessage(messageId, { respondedAction: "confirm" });
|
||||
|
||||
// Perform the actual event operation
|
||||
let content: string;
|
||||
|
||||
if (action === "create" && event) {
|
||||
@@ -298,12 +314,12 @@ export class ChatService {
|
||||
content = "Ungültige Aktion.";
|
||||
}
|
||||
|
||||
const message: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
conversationId,
|
||||
// Save response message to DB
|
||||
const message = await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "assistant",
|
||||
content,
|
||||
};
|
||||
});
|
||||
|
||||
return { message, conversationId };
|
||||
}
|
||||
|
||||
@@ -312,17 +328,34 @@ export class ChatService {
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
): Promise<ChatResponse> {
|
||||
const message: ChatMessage = {
|
||||
id: Date.now().toString(),
|
||||
conversationId,
|
||||
// Update original message with respondedAction
|
||||
await this.chatRepo.updateMessage(messageId, { respondedAction: "reject" });
|
||||
|
||||
// Save response message to DB
|
||||
const message = await this.chatRepo.createMessage(conversationId, {
|
||||
sender: "assistant",
|
||||
content: "Der Vorschlag wurde abgelehnt.",
|
||||
};
|
||||
});
|
||||
|
||||
return { message, conversationId };
|
||||
}
|
||||
|
||||
async getConversations(userId: string): Promise<ConversationSummary[]> {
|
||||
throw new Error("Not implemented");
|
||||
const conversations = await this.chatRepo.getConversationsByUser(userId);
|
||||
|
||||
// For each conversation, get the last message
|
||||
const summaries: ConversationSummary[] = await Promise.all(
|
||||
conversations.map(async (conv) => {
|
||||
const messages = await this.chatRepo.getMessages(conv.id, { limit: 1 });
|
||||
return {
|
||||
id: conv.id,
|
||||
lastMessage: messages[0],
|
||||
createdAt: conv.createdAt,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return summaries;
|
||||
}
|
||||
|
||||
async getConversation(
|
||||
@@ -330,6 +363,14 @@ export class ChatService {
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]> {
|
||||
throw new Error("Not implemented");
|
||||
// Verify conversation belongs to user
|
||||
const conversations = await this.chatRepo.getConversationsByUser(userId);
|
||||
const conversation = conversations.find((c) => c.id === conversationId);
|
||||
|
||||
if (!conversation) {
|
||||
throw new Error("Conversation not found");
|
||||
}
|
||||
|
||||
return this.chatRepo.getMessages(conversationId, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Conversation,
|
||||
CreateMessageDTO,
|
||||
GetMessagesOptions,
|
||||
UpdateMessageDTO,
|
||||
} from "@caldav/shared";
|
||||
|
||||
export interface ChatRepository {
|
||||
@@ -15,8 +16,14 @@ export interface ChatRepository {
|
||||
conversationId: string,
|
||||
options?: GetMessagesOptions,
|
||||
): Promise<ChatMessage[]>;
|
||||
|
||||
createMessage(
|
||||
conversationId: string,
|
||||
message: CreateMessageDTO,
|
||||
): Promise<ChatMessage>;
|
||||
|
||||
updateMessage(
|
||||
messageId: string,
|
||||
updates: UpdateMessageDTO,
|
||||
): Promise<ChatMessage | null>;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ export type MessageSender = "user" | "assistant";
|
||||
|
||||
export type EventAction = "create" | "update" | "delete";
|
||||
|
||||
export type RespondedAction = "confirm" | "reject";
|
||||
|
||||
export interface ProposedEventChange {
|
||||
action: EventAction;
|
||||
eventId?: string; // Required for update/delete
|
||||
@@ -17,6 +19,7 @@ export interface ChatMessage {
|
||||
sender: MessageSender;
|
||||
content: string;
|
||||
proposedChange?: ProposedEventChange;
|
||||
respondedAction?: RespondedAction;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
@@ -43,6 +46,10 @@ export interface GetMessagesOptions {
|
||||
limit?: number; // Default: 20
|
||||
}
|
||||
|
||||
export interface UpdateMessageDTO {
|
||||
respondedAction?: RespondedAction;
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
message: ChatMessage;
|
||||
conversationId: string;
|
||||
|
||||
Reference in New Issue
Block a user