Compare commits
2 Commits
d86b18173f
...
675785ec93
| Author | SHA1 | Date | |
|---|---|---|---|
| 675785ec93 | |||
| c897b6d680 |
42
CLAUDE.md
42
CLAUDE.md
@@ -13,6 +13,7 @@ This is a fullstack TypeScript monorepo with npm workspaces.
|
|||||||
### Root (monorepo)
|
### Root (monorepo)
|
||||||
```bash
|
```bash
|
||||||
npm install # Install all dependencies for all workspaces
|
npm install # Install all dependencies for all workspaces
|
||||||
|
npm run format # Format all TypeScript files with Prettier
|
||||||
```
|
```
|
||||||
|
|
||||||
### Client (apps/client) - Expo React Native app
|
### Client (apps/client) - Expo React Native app
|
||||||
@@ -44,7 +45,7 @@ npm run start -w @caldav/server # Run compiled server (port 3000)
|
|||||||
| Backend | Express.js | Web framework |
|
| Backend | Express.js | Web framework |
|
||||||
| | MongoDB | Database |
|
| | MongoDB | Database |
|
||||||
| | Mongoose | ODM |
|
| | Mongoose | ODM |
|
||||||
| | Claude (Anthropic) | AI/LLM for chat |
|
| | GPT (OpenAI) | AI/LLM for chat |
|
||||||
| | JWT | Authentication |
|
| | JWT | Authentication |
|
||||||
| Planned | iCalendar | Event export/import |
|
| Planned | iCalendar | Event export/import |
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ src/
|
|||||||
└── stores/ # Zustand state management
|
└── stores/ # Zustand state management
|
||||||
├── index.ts # Re-exports all stores
|
├── index.ts # Re-exports all stores
|
||||||
├── AuthStore.ts # user, token, isAuthenticated, login(), logout(), setToken()
|
├── 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()
|
└── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent()
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -134,7 +135,14 @@ src/
|
|||||||
│ ├── MongoEventRepository.ts
|
│ ├── MongoEventRepository.ts
|
||||||
│ └── MongoChatRepository.ts
|
│ └── MongoChatRepository.ts
|
||||||
├── ai/
|
├── ai/
|
||||||
│ └── ClaudeAdapter.ts # Implements AIProvider
|
│ ├── GPTAdapter.ts # Implements AIProvider using OpenAI GPT
|
||||||
|
│ ├── index.ts # Re-exports GPTAdapter
|
||||||
|
│ └── utils/ # Shared AI utilities (provider-agnostic)
|
||||||
|
│ ├── index.ts # Re-exports
|
||||||
|
│ ├── eventFormatter.ts # formatExistingEvents() for system prompt
|
||||||
|
│ ├── systemPrompt.ts # buildSystemPrompt() - German calendar assistant prompt
|
||||||
|
│ ├── toolDefinitions.ts # TOOL_DEFINITIONS - provider-agnostic tool specs
|
||||||
|
│ └── toolExecutor.ts # executeToolCall() - handles getDay, proposeCreate/Update/Delete, searchEvents
|
||||||
└── utils/
|
└── utils/
|
||||||
├── jwt.ts # signToken(), verifyToken()
|
├── jwt.ts # signToken(), verifyToken()
|
||||||
├── password.ts # hash(), compare()
|
├── password.ts # hash(), compare()
|
||||||
@@ -172,7 +180,7 @@ src/
|
|||||||
│ ├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO, ExpandedEvent
|
│ ├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO, ExpandedEvent
|
||||||
│ ├── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
|
│ ├── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
|
||||||
│ │ # GetMessagesOptions, ChatResponse, ConversationSummary,
|
│ │ # GetMessagesOptions, ChatResponse, ConversationSummary,
|
||||||
│ │ # ProposedEventChange, EventAction
|
│ │ # ProposedEventChange, EventAction, RespondedAction, UpdateMessageDTO
|
||||||
│ └── Constants.ts # DAYS, MONTHS, Day, Month, DAY_INDEX, DAY_INDEX_TO_DAY,
|
│ └── Constants.ts # DAYS, MONTHS, Day, Month, DAY_INDEX, DAY_INDEX_TO_DAY,
|
||||||
│ # DAY_TO_GERMAN, DAY_TO_GERMAN_SHORT, MONTH_TO_GERMAN
|
│ # DAY_TO_GERMAN, DAY_TO_GERMAN_SHORT, MONTH_TO_GERMAN
|
||||||
└── utils/
|
└── utils/
|
||||||
@@ -184,12 +192,14 @@ src/
|
|||||||
- `User`: id, email, displayName, passwordHash?, createdAt?, updatedAt?
|
- `User`: id, email, displayName, passwordHash?, createdAt?, updatedAt?
|
||||||
- `CalendarEvent`: id, userId, title, description?, startTime, endTime, note?, isRecurring?, recurrenceRule?
|
- `CalendarEvent`: id, userId, title, description?, startTime, endTime, note?, isRecurring?, recurrenceRule?
|
||||||
- `ExpandedEvent`: Extends CalendarEvent with occurrenceStart, occurrenceEnd (for recurring event instances)
|
- `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?
|
- `ProposedEventChange`: action ('create' | 'update' | 'delete'), eventId?, event?, updates?
|
||||||
- `Conversation`: id, userId, createdAt?, updatedAt? (messages loaded separately via lazy loading)
|
- `Conversation`: id, userId, createdAt?, updatedAt? (messages loaded separately via lazy loading)
|
||||||
- `CreateEventDTO`: Used for creating events AND for AI-proposed events
|
- `CreateEventDTO`: Used for creating events AND for AI-proposed events
|
||||||
- `GetMessagesOptions`: Cursor-based pagination with `before?: string` and `limit?: number`
|
- `GetMessagesOptions`: Cursor-based pagination with `before?: string` and `limit?: number`
|
||||||
- `ConversationSummary`: id, lastMessage?, createdAt? (for conversation list)
|
- `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"
|
- `Day`: "Monday" | "Tuesday" | ... | "Sunday"
|
||||||
- `Month`: "January" | "February" | ... | "December"
|
- `Month`: "January" | "February" | ... | "December"
|
||||||
|
|
||||||
@@ -262,6 +272,8 @@ Server requires `.env` file in `apps/server/`:
|
|||||||
JWT_SECRET=your-secret-key
|
JWT_SECRET=your-secret-key
|
||||||
JWT_EXPIRES_IN=1h
|
JWT_EXPIRES_IN=1h
|
||||||
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
||||||
|
OPENAI_API_KEY=sk-proj-...
|
||||||
|
USE_TEST_RESPONSES=false # true = static test responses, false = real GPT AI
|
||||||
```
|
```
|
||||||
|
|
||||||
## Current Implementation Status
|
## Current Implementation Status
|
||||||
@@ -281,14 +293,16 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
|||||||
- `EventService`: Full CRUD with recurring event expansion via recurrenceExpander
|
- `EventService`: Full CRUD with recurring event expansion via recurrenceExpander
|
||||||
- `utils/eventFormatters`: getWeeksOverview(), getMonthOverview() with German localization
|
- `utils/eventFormatters`: getWeeksOverview(), getMonthOverview() with German localization
|
||||||
- `utils/recurrenceExpander`: expandRecurringEvents() using rrule library for RRULE parsing
|
- `utils/recurrenceExpander`: expandRecurringEvents() using rrule library for RRULE parsing
|
||||||
|
- `ChatController`: getConversations(), getConversation() with cursor-based pagination support
|
||||||
|
- `ChatService`: getConversations(), getConversation(), processMessage() uses real AI or test responses (via USE_TEST_RESPONSES), 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
|
||||||
|
- `GPTAdapter`: Full implementation with OpenAI GPT (gpt-5-mini model), function calling for calendar operations
|
||||||
|
- `ai/utils/`: Provider-agnostic shared utilities (systemPrompt, toolDefinitions, toolExecutor, eventFormatter)
|
||||||
- **Stubbed (TODO):**
|
- **Stubbed (TODO):**
|
||||||
- `AuthMiddleware.authenticate()`: Currently uses fake user for testing
|
- `AuthMiddleware.authenticate()`: Currently uses fake user for testing
|
||||||
- `AuthController`: refresh(), logout()
|
- `AuthController`: refresh(), logout()
|
||||||
- `AuthService`: refreshToken()
|
- `AuthService`: refreshToken()
|
||||||
- `ChatController`: getConversations(), getConversation()
|
|
||||||
- `MongoChatRepository`: Database persistence for chat
|
|
||||||
- **Not started:**
|
|
||||||
- `ClaudeAdapter` (AI integration - currently using test responses)
|
|
||||||
|
|
||||||
**Shared:** Types, DTOs, constants (Day, Month with German translations), ExpandedEvent type, and date utilities defined and exported.
|
**Shared:** Types, DTOs, constants (Day, Month with German translations), ExpandedEvent type, and date utilities defined and exported.
|
||||||
|
|
||||||
@@ -302,20 +316,22 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
|||||||
- Tap-to-open modal overlay showing EventCards for selected day
|
- Tap-to-open modal overlay showing EventCards for selected day
|
||||||
- Supports events from adjacent months visible in grid
|
- Supports events from adjacent months visible in grid
|
||||||
- Uses `useFocusEffect` for automatic reload on tab focus
|
- Uses `useFocusEffect` for automatic reload on tab focus
|
||||||
- Chat screen functional with FlashList, message sending, and event confirm/reject
|
- Chat screen fully functional with FlashList, message sending, and event confirm/reject
|
||||||
- Messages persisted via ChatStore (survives tab switches)
|
- 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)
|
- KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height)
|
||||||
- Auto-scroll to end on new messages and keyboard show
|
- Auto-scroll to end on new messages and keyboard show
|
||||||
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
||||||
- `ApiClient`: get(), post(), put(), delete() implemented
|
- `ApiClient`: get(), post(), put(), delete() implemented
|
||||||
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully 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
|
- `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
|
- `EventCard`: Uses EventCardBase + edit/delete buttons for calendar display
|
||||||
- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions)
|
- `ProposedEventCard`: Uses EventCardBase + confirm/reject buttons for chat proposals (supports create/update/delete actions)
|
||||||
- `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator, secondaryBg
|
- `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator, secondaryBg
|
||||||
- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[]
|
- `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
|
- Auth screens (Login, Register), Event Detail, and Note screens exist as skeletons
|
||||||
- AuthStore defined with `throw new Error('Not implemented')`
|
- AuthStore defined with `throw new Error('Not implemented')`
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
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 currentTheme from "../../Themes";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import Header from "../../components/Header";
|
import Header from "../../components/Header";
|
||||||
import BaseBackground from "../../components/BaseBackground";
|
import BaseBackground from "../../components/BaseBackground";
|
||||||
import { FlashList } from "@shopify/flash-list";
|
import { FlashList } from "@shopify/flash-list";
|
||||||
import { ChatService } from "../../services";
|
import { ChatService } from "../../services";
|
||||||
import { useChatStore, MessageData } from "../../stores";
|
import {
|
||||||
|
useChatStore,
|
||||||
|
chatMessageToMessageData,
|
||||||
|
MessageData,
|
||||||
|
} from "../../stores";
|
||||||
import { ProposedEventChange } from "@caldav/shared";
|
import { ProposedEventChange } from "@caldav/shared";
|
||||||
import { ProposedEventCard } from "../../components/ProposedEventCard";
|
import { ProposedEventCard } from "../../components/ProposedEventCard";
|
||||||
|
|
||||||
// TODO: better shadows for everything
|
// TODO: better shadows for everything
|
||||||
// (maybe with extra library because of differences between android and ios)
|
// (maybe with extra library because of differences between android and ios)
|
||||||
// TODO: max width for messages
|
// TODO: max width for messages
|
||||||
// TODO: create new messages
|
|
||||||
|
|
||||||
type BubbleSide = "left" | "right";
|
type BubbleSide = "left" | "right";
|
||||||
|
|
||||||
@@ -30,14 +41,43 @@ type ChatInputProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Chat = () => {
|
const Chat = () => {
|
||||||
const { messages, addMessage, updateMessage } = useChatStore();
|
const { messages, addMessage, addMessages, updateMessage } = useChatStore();
|
||||||
const listRef = useRef<FlashList<MessageData>>(null);
|
const listRef =
|
||||||
|
useRef<React.ComponentRef<typeof FlashList<MessageData>>>(null);
|
||||||
|
const [currentConversationId, setCurrentConversationId] = useState<
|
||||||
|
string | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyboardDidShow = Keyboard.addListener("keyboardDidShow", scrollToEnd);
|
const keyboardDidShow = Keyboard.addListener(
|
||||||
|
"keyboardDidShow",
|
||||||
|
scrollToEnd,
|
||||||
|
);
|
||||||
return () => keyboardDidShow.remove();
|
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 = () => {
|
const scrollToEnd = () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
listRef.current?.scrollToEnd({ animated: true });
|
listRef.current?.scrollToEnd({ animated: true });
|
||||||
@@ -87,13 +127,22 @@ const Chat = () => {
|
|||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
side: "right",
|
side: "right",
|
||||||
content: text,
|
content: text,
|
||||||
|
conversationId: currentConversationId,
|
||||||
};
|
};
|
||||||
addMessage(userMessage);
|
addMessage(userMessage);
|
||||||
scrollToEnd();
|
scrollToEnd();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch server response
|
// Fetch server response (include conversationId for existing conversations)
|
||||||
const response = await ChatService.sendMessage({ content: text });
|
const response = await ChatService.sendMessage({
|
||||||
|
content: text,
|
||||||
|
conversationId: currentConversationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track conversation ID for subsequent messages
|
||||||
|
if (!currentConversationId) {
|
||||||
|
setCurrentConversationId(response.conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
// Show bot response
|
// Show bot response
|
||||||
const botMessage: MessageData = {
|
const botMessage: MessageData = {
|
||||||
|
|||||||
@@ -47,13 +47,20 @@ export const ChatService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getConversations: async (): Promise<ConversationSummary[]> => {
|
getConversations: async (): Promise<ConversationSummary[]> => {
|
||||||
throw new Error("Not implemented");
|
return ApiClient.get<ConversationSummary[]>("/chat/conversations");
|
||||||
},
|
},
|
||||||
|
|
||||||
getConversation: async (
|
getConversation: async (
|
||||||
_id: string,
|
id: string,
|
||||||
_options?: GetMessagesOptions,
|
options?: GetMessagesOptions,
|
||||||
): Promise<ChatMessage[]> => {
|
): 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 { create } from "zustand";
|
||||||
import { ProposedEventChange } from "@caldav/shared";
|
import { ChatMessage, ProposedEventChange } from "@caldav/shared";
|
||||||
|
|
||||||
type BubbleSide = "left" | "right";
|
type BubbleSide = "left" | "right";
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ export type MessageData = {
|
|||||||
|
|
||||||
interface ChatState {
|
interface ChatState {
|
||||||
messages: MessageData[];
|
messages: MessageData[];
|
||||||
|
addMessages: (messages: MessageData[]) => void;
|
||||||
addMessage: (message: MessageData) => void;
|
addMessage: (message: MessageData) => void;
|
||||||
updateMessage: (id: string, updates: Partial<MessageData>) => void;
|
updateMessage: (id: string, updates: Partial<MessageData>) => void;
|
||||||
clearMessages: () => void;
|
clearMessages: () => void;
|
||||||
@@ -21,6 +22,9 @@ interface ChatState {
|
|||||||
|
|
||||||
export const useChatStore = create<ChatState>((set) => ({
|
export const useChatStore = create<ChatState>((set) => ({
|
||||||
messages: [],
|
messages: [],
|
||||||
|
addMessages(messages) {
|
||||||
|
set((state) => ({ messages: [...state.messages, ...messages] }));
|
||||||
|
},
|
||||||
addMessage: (message: MessageData) => {
|
addMessage: (message: MessageData) => {
|
||||||
set((state) => ({ messages: [...state.messages, message] }));
|
set((state) => ({ messages: [...state.messages, message] }));
|
||||||
},
|
},
|
||||||
@@ -35,3 +39,15 @@ export const useChatStore = create<ChatState>((set) => ({
|
|||||||
set({ messages: [] });
|
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,7 @@
|
|||||||
export { useAuthStore } from "./AuthStore";
|
export { useAuthStore } from "./AuthStore";
|
||||||
export { useChatStore, type MessageData } from "./ChatStore";
|
export {
|
||||||
|
useChatStore,
|
||||||
|
chatMessageToMessageData,
|
||||||
|
type MessageData,
|
||||||
|
} from "./ChatStore";
|
||||||
export { useEventsStore } from "./EventsStore";
|
export { useEventsStore } from "./EventsStore";
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
"start": "node dist/app.js"
|
"start": "node dist/app.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.71.2",
|
|
||||||
"@caldav/shared": "*",
|
"@caldav/shared": "*",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.1.1",
|
"mongoose": "^9.1.1",
|
||||||
|
"openai": "^6.15.0",
|
||||||
"rrule": "^2.8.1"
|
"rrule": "^2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import Anthropic from "@anthropic-ai/sdk";
|
|
||||||
import { AIProvider, AIContext, AIResponse } from "../services/interfaces";
|
|
||||||
|
|
||||||
export class ClaudeAdapter implements AIProvider {
|
|
||||||
private client: Anthropic;
|
|
||||||
private model: string;
|
|
||||||
|
|
||||||
constructor(apiKey?: string, model: string = "claude-3-haiku-20240307") {
|
|
||||||
this.client = new Anthropic({
|
|
||||||
apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
|
|
||||||
});
|
|
||||||
this.model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
async processMessage(
|
|
||||||
message: string,
|
|
||||||
context: AIContext,
|
|
||||||
): Promise<AIResponse> {
|
|
||||||
throw new Error("Not implemented");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
116
apps/server/src/ai/GPTAdapter.ts
Normal file
116
apps/server/src/ai/GPTAdapter.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import OpenAI from "openai";
|
||||||
|
import { ProposedEventChange } from "@caldav/shared";
|
||||||
|
import { AIProvider, AIContext, AIResponse } from "../services/interfaces";
|
||||||
|
import {
|
||||||
|
buildSystemPrompt,
|
||||||
|
TOOL_DEFINITIONS,
|
||||||
|
executeToolCall,
|
||||||
|
ToolDefinition,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert tool definitions to OpenAI format.
|
||||||
|
*/
|
||||||
|
function toOpenAITools(
|
||||||
|
defs: ToolDefinition[],
|
||||||
|
): OpenAI.Chat.Completions.ChatCompletionTool[] {
|
||||||
|
return defs.map((def) => ({
|
||||||
|
type: "function" as const,
|
||||||
|
function: {
|
||||||
|
name: def.name,
|
||||||
|
description: def.description,
|
||||||
|
parameters: def.parameters,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GPTAdapter implements AIProvider {
|
||||||
|
private client: OpenAI;
|
||||||
|
private model: string;
|
||||||
|
private tools: OpenAI.Chat.Completions.ChatCompletionTool[];
|
||||||
|
|
||||||
|
constructor(apiKey?: string, model: string = "gpt-5-mini") {
|
||||||
|
this.client = new OpenAI({
|
||||||
|
apiKey: apiKey || process.env.OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
this.model = model;
|
||||||
|
this.tools = toOpenAITools(TOOL_DEFINITIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
async processMessage(
|
||||||
|
message: string,
|
||||||
|
context: AIContext,
|
||||||
|
): Promise<AIResponse> {
|
||||||
|
const systemPrompt = buildSystemPrompt(context);
|
||||||
|
|
||||||
|
// Build messages array with conversation history
|
||||||
|
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
|
||||||
|
{ role: "developer", content: systemPrompt },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add conversation history
|
||||||
|
for (const msg of context.conversationHistory) {
|
||||||
|
messages.push({
|
||||||
|
role: msg.sender === "user" ? "user" : "assistant",
|
||||||
|
content: msg.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current user message
|
||||||
|
messages.push({ role: "user", content: message });
|
||||||
|
|
||||||
|
let proposedChange: ProposedEventChange | undefined;
|
||||||
|
|
||||||
|
// Tool calling loop
|
||||||
|
while (true) {
|
||||||
|
const response = await this.client.chat.completions.create({
|
||||||
|
model: this.model,
|
||||||
|
messages,
|
||||||
|
tools: this.tools,
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistantMessage = response.choices[0].message;
|
||||||
|
|
||||||
|
// If no tool calls, return the final response
|
||||||
|
if (
|
||||||
|
!assistantMessage.tool_calls ||
|
||||||
|
assistantMessage.tool_calls.length === 0
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
content:
|
||||||
|
assistantMessage.content || "Ich konnte keine Antwort generieren.",
|
||||||
|
proposedChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process tool calls
|
||||||
|
for (const toolCall of assistantMessage.tool_calls) {
|
||||||
|
// Skip non-function tool calls
|
||||||
|
if (toolCall.type !== "function") continue;
|
||||||
|
|
||||||
|
const { name, arguments: argsRaw } = toolCall.function;
|
||||||
|
const args = JSON.parse(argsRaw);
|
||||||
|
|
||||||
|
const result = executeToolCall(name, args, context);
|
||||||
|
|
||||||
|
// If the tool returned a proposedChange, capture it
|
||||||
|
if (result.proposedChange) {
|
||||||
|
proposedChange = result.proposedChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add assistant message with tool call
|
||||||
|
messages.push({
|
||||||
|
role: "assistant",
|
||||||
|
tool_calls: [toolCall],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add tool result
|
||||||
|
messages.push({
|
||||||
|
role: "tool",
|
||||||
|
tool_call_id: toolCall.id,
|
||||||
|
content: result.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
export * from "./ClaudeAdapter";
|
export * from "./GPTAdapter";
|
||||||
|
|||||||
29
apps/server/src/ai/utils/eventFormatter.ts
Normal file
29
apps/server/src/ai/utils/eventFormatter.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { CalendarEvent } from "@caldav/shared";
|
||||||
|
|
||||||
|
// German date/time formatting helpers
|
||||||
|
export const formatDate = (d: Date) => d.toLocaleDateString("de-DE");
|
||||||
|
export const formatTime = (d: Date) =>
|
||||||
|
d.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||||
|
export const formatDateTime = (d: Date) =>
|
||||||
|
`${formatDate(d)} ${d.toLocaleTimeString("de-DE")}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a list of events for display in the system prompt.
|
||||||
|
* Output is in German with date/time formatting.
|
||||||
|
*/
|
||||||
|
export function formatExistingEvents(events: CalendarEvent[]): string {
|
||||||
|
if (events.length === 0) {
|
||||||
|
return "Keine Termine vorhanden.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return events
|
||||||
|
.map((e) => {
|
||||||
|
const start = new Date(e.startTime);
|
||||||
|
const end = new Date(e.endTime);
|
||||||
|
const timeStr = `${formatTime(start)} - ${formatTime(end)}`;
|
||||||
|
const recurring = e.isRecurring ? " (wiederkehrend)" : "";
|
||||||
|
const desc = e.description ? ` | ${e.description}` : "";
|
||||||
|
return `- ${e.title} (ID: ${e.id}) | ${formatDate(start)} ${timeStr}${recurring}${desc}`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
13
apps/server/src/ai/utils/index.ts
Normal file
13
apps/server/src/ai/utils/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export {
|
||||||
|
formatExistingEvents,
|
||||||
|
formatDate,
|
||||||
|
formatTime,
|
||||||
|
formatDateTime,
|
||||||
|
} from "./eventFormatter";
|
||||||
|
export { buildSystemPrompt } from "./systemPrompt";
|
||||||
|
export {
|
||||||
|
TOOL_DEFINITIONS,
|
||||||
|
type ToolDefinition,
|
||||||
|
type ParameterDef,
|
||||||
|
} from "./toolDefinitions";
|
||||||
|
export { executeToolCall, type ToolResult } from "./toolExecutor";
|
||||||
37
apps/server/src/ai/utils/systemPrompt.ts
Normal file
37
apps/server/src/ai/utils/systemPrompt.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { AIContext } from "../../services/interfaces";
|
||||||
|
import { formatExistingEvents } from "./eventFormatter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the system prompt for the AI assistant.
|
||||||
|
* This prompt is provider-agnostic and can be used with any LLM.
|
||||||
|
*/
|
||||||
|
export function buildSystemPrompt(context: AIContext): string {
|
||||||
|
const currentDate = context.currentDate.toLocaleDateString("de-DE", {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventsText = formatExistingEvents(context.existingEvents);
|
||||||
|
|
||||||
|
return `Du bist ein hilfreicher Kalender-Assistent für die App "CalChat".
|
||||||
|
Du hilfst Benutzern beim Erstellen, Ändern und Löschen von Terminen.
|
||||||
|
Antworte immer auf Deutsch.
|
||||||
|
|
||||||
|
Aktuelles Datum und Uhrzeit: ${currentDate}
|
||||||
|
|
||||||
|
Wichtige Regeln:
|
||||||
|
- Nutze getDay() für relative Datumsberechnungen (z.B. "nächsten Freitag um 14 Uhr")
|
||||||
|
- Wenn der Benutzer einen Termin erstellen will, nutze proposeCreateEvent
|
||||||
|
- Wenn der Benutzer einen Termin ändern will, nutze proposeUpdateEvent mit der Event-ID
|
||||||
|
- Wenn der Benutzer einen Termin löschen will, nutze proposeDeleteEvent mit der Event-ID
|
||||||
|
- Du kannst NUR EINEN Event-Vorschlag pro Antwort machen
|
||||||
|
- Wenn der Benutzer nach seinen Terminen fragt, nutze die unten stehende Liste
|
||||||
|
- Nutze searchEvents um nach Terminen zu suchen, wenn du die genaue ID brauchst
|
||||||
|
|
||||||
|
Existierende Termine des Benutzers:
|
||||||
|
${eventsText}`;
|
||||||
|
}
|
||||||
170
apps/server/src/ai/utils/toolDefinitions.ts
Normal file
170
apps/server/src/ai/utils/toolDefinitions.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* Parameter definition for tool parameters.
|
||||||
|
*/
|
||||||
|
export interface ParameterDef {
|
||||||
|
type: "string" | "number" | "boolean" | "object" | "array";
|
||||||
|
description?: string;
|
||||||
|
enum?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider-agnostic tool definition format.
|
||||||
|
* Can be converted to OpenAI, Claude, or other provider formats.
|
||||||
|
*/
|
||||||
|
export interface ToolDefinition {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: {
|
||||||
|
type: "object";
|
||||||
|
properties: Record<string, ParameterDef>;
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All available tools for the calendar assistant.
|
||||||
|
*/
|
||||||
|
export const TOOL_DEFINITIONS: ToolDefinition[] = [
|
||||||
|
{
|
||||||
|
name: "getDay",
|
||||||
|
description:
|
||||||
|
"Get a date for a specific weekday relative to today. Returns an ISO date string.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
day: {
|
||||||
|
type: "string",
|
||||||
|
enum: [
|
||||||
|
"Monday",
|
||||||
|
"Tuesday",
|
||||||
|
"Wednesday",
|
||||||
|
"Thursday",
|
||||||
|
"Friday",
|
||||||
|
"Saturday",
|
||||||
|
"Sunday",
|
||||||
|
],
|
||||||
|
description: "The target weekday",
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
type: "number",
|
||||||
|
description:
|
||||||
|
"1 = next occurrence, 2 = the one after, -1 = last occurrence, etc.",
|
||||||
|
},
|
||||||
|
hour: {
|
||||||
|
type: "number",
|
||||||
|
description: "Hour of day (0-23)",
|
||||||
|
},
|
||||||
|
minute: {
|
||||||
|
type: "number",
|
||||||
|
description: "Minute (0-59)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["day", "offset", "hour", "minute"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "getCurrentDateTime",
|
||||||
|
description: "Get the current date and time as an ISO string",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "proposeCreateEvent",
|
||||||
|
description:
|
||||||
|
"Propose creating a new calendar event. The user must confirm before it's saved. Call this when the user wants to create a new appointment.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: "string",
|
||||||
|
description: "Event title",
|
||||||
|
},
|
||||||
|
startTime: {
|
||||||
|
type: "string",
|
||||||
|
description: "Start time as ISO date string",
|
||||||
|
},
|
||||||
|
endTime: {
|
||||||
|
type: "string",
|
||||||
|
description: "End time as ISO date string",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
description: "Optional event description",
|
||||||
|
},
|
||||||
|
isRecurring: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether this is a recurring event",
|
||||||
|
},
|
||||||
|
recurrenceRule: {
|
||||||
|
type: "string",
|
||||||
|
description: "RRULE format string for recurring events",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["title", "startTime", "endTime"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "proposeUpdateEvent",
|
||||||
|
description:
|
||||||
|
"Propose updating an existing event. The user must confirm. Use this when the user wants to modify an appointment.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
eventId: {
|
||||||
|
type: "string",
|
||||||
|
description: "ID of the event to update",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: "string",
|
||||||
|
description: "New title (optional)",
|
||||||
|
},
|
||||||
|
startTime: {
|
||||||
|
type: "string",
|
||||||
|
description: "New start time as ISO date string (optional)",
|
||||||
|
},
|
||||||
|
endTime: {
|
||||||
|
type: "string",
|
||||||
|
description: "New end time as ISO date string (optional)",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
description: "New description (optional)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["eventId"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "proposeDeleteEvent",
|
||||||
|
description:
|
||||||
|
"Propose deleting an event. The user must confirm. Use this when the user wants to remove an appointment.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
eventId: {
|
||||||
|
type: "string",
|
||||||
|
description: "ID of the event to delete",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["eventId"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "searchEvents",
|
||||||
|
description:
|
||||||
|
"Search for events by title in the user's calendar. Returns matching events.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: "string",
|
||||||
|
description: "Search query to match against event titles",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
156
apps/server/src/ai/utils/toolExecutor.ts
Normal file
156
apps/server/src/ai/utils/toolExecutor.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import {
|
||||||
|
ProposedEventChange,
|
||||||
|
getDay,
|
||||||
|
Day,
|
||||||
|
DAY_TO_GERMAN,
|
||||||
|
} from "@caldav/shared";
|
||||||
|
import { AIContext } from "../../services/interfaces";
|
||||||
|
import { formatDate, formatTime, formatDateTime } from "./eventFormatter";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of executing a tool call.
|
||||||
|
*/
|
||||||
|
export interface ToolResult {
|
||||||
|
content: string;
|
||||||
|
proposedChange?: ProposedEventChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a tool call and return the result.
|
||||||
|
* This function is provider-agnostic and can be used with any LLM.
|
||||||
|
*/
|
||||||
|
export function executeToolCall(
|
||||||
|
name: string,
|
||||||
|
args: Record<string, unknown>,
|
||||||
|
context: AIContext,
|
||||||
|
): ToolResult {
|
||||||
|
switch (name) {
|
||||||
|
case "getDay": {
|
||||||
|
const date = getDay(
|
||||||
|
args.day as Day,
|
||||||
|
args.offset as number,
|
||||||
|
args.hour as number,
|
||||||
|
args.minute as number,
|
||||||
|
);
|
||||||
|
const dayName = DAY_TO_GERMAN[args.day as Day];
|
||||||
|
return {
|
||||||
|
content: `${date.toISOString()} (${dayName}, ${formatDate(date)} um ${formatTime(date)} Uhr)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "getCurrentDateTime": {
|
||||||
|
const now = context.currentDate;
|
||||||
|
return {
|
||||||
|
content: `${now.toISOString()} (${formatDateTime(now)})`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "proposeCreateEvent": {
|
||||||
|
const event = {
|
||||||
|
title: args.title as string,
|
||||||
|
startTime: new Date(args.startTime as string),
|
||||||
|
endTime: new Date(args.endTime as string),
|
||||||
|
description: args.description as string | undefined,
|
||||||
|
isRecurring: args.isRecurring as boolean | undefined,
|
||||||
|
recurrenceRule: args.recurrenceRule as string | undefined,
|
||||||
|
};
|
||||||
|
const dateStr = formatDate(event.startTime);
|
||||||
|
const startStr = formatTime(event.startTime);
|
||||||
|
const endStr = formatTime(event.endTime);
|
||||||
|
return {
|
||||||
|
content: `Event-Vorschlag erstellt: "${event.title}" am ${dateStr} von ${startStr} bis ${endStr} Uhr`,
|
||||||
|
proposedChange: {
|
||||||
|
action: "create",
|
||||||
|
event,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "proposeUpdateEvent": {
|
||||||
|
const eventId = args.eventId as string;
|
||||||
|
const existingEvent = context.existingEvents.find(
|
||||||
|
(e) => e.id === eventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingEvent) {
|
||||||
|
return { content: `Event mit ID ${eventId} nicht gefunden.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: Record<string, unknown> = {};
|
||||||
|
if (args.title) updates.title = args.title;
|
||||||
|
if (args.startTime)
|
||||||
|
updates.startTime = new Date(args.startTime as string);
|
||||||
|
if (args.endTime) updates.endTime = new Date(args.endTime as string);
|
||||||
|
if (args.description) updates.description = args.description;
|
||||||
|
|
||||||
|
// Build event object for display (merge existing with updates)
|
||||||
|
const displayEvent = {
|
||||||
|
title: (updates.title as string) || existingEvent.title,
|
||||||
|
startTime: (updates.startTime as Date) || existingEvent.startTime,
|
||||||
|
endTime: (updates.endTime as Date) || existingEvent.endTime,
|
||||||
|
description:
|
||||||
|
(updates.description as string) || existingEvent.description,
|
||||||
|
isRecurring: existingEvent.isRecurring,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: `Update-Vorschlag für "${existingEvent.title}" erstellt.`,
|
||||||
|
proposedChange: {
|
||||||
|
action: "update",
|
||||||
|
eventId,
|
||||||
|
updates,
|
||||||
|
event: displayEvent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "proposeDeleteEvent": {
|
||||||
|
const eventId = args.eventId as string;
|
||||||
|
const existingEvent = context.existingEvents.find(
|
||||||
|
(e) => e.id === eventId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingEvent) {
|
||||||
|
return { content: `Event mit ID ${eventId} nicht gefunden.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: `Lösch-Vorschlag für "${existingEvent.title}" erstellt.`,
|
||||||
|
proposedChange: {
|
||||||
|
action: "delete",
|
||||||
|
eventId,
|
||||||
|
event: {
|
||||||
|
title: existingEvent.title,
|
||||||
|
startTime: existingEvent.startTime,
|
||||||
|
endTime: existingEvent.endTime,
|
||||||
|
description: existingEvent.description,
|
||||||
|
isRecurring: existingEvent.isRecurring,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "searchEvents": {
|
||||||
|
const query = (args.query as string).toLowerCase();
|
||||||
|
const matches = context.existingEvents.filter((e) =>
|
||||||
|
e.title.toLowerCase().includes(query),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return { content: `Keine Termine mit "${args.query}" gefunden.` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = matches
|
||||||
|
.map((e) => {
|
||||||
|
const start = new Date(e.startTime);
|
||||||
|
return `- ${e.title} (ID: ${e.id}) am ${formatDate(start)} um ${formatTime(start)} Uhr`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return { content: `Gefundene Termine:\n${results}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { content: `Unbekannte Funktion: ${name}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
MongoEventRepository,
|
MongoEventRepository,
|
||||||
MongoChatRepository,
|
MongoChatRepository,
|
||||||
} from "./repositories";
|
} from "./repositories";
|
||||||
import { ClaudeAdapter } from "./ai";
|
import { GPTAdapter } from "./ai";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
@@ -43,7 +43,7 @@ const eventRepo = new MongoEventRepository();
|
|||||||
const chatRepo = new MongoChatRepository();
|
const chatRepo = new MongoChatRepository();
|
||||||
|
|
||||||
// Initialize AI provider
|
// Initialize AI provider
|
||||||
const aiProvider = new ClaudeAdapter();
|
const aiProvider = new GPTAdapter();
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
const authService = new AuthService(userRepo);
|
const authService = new AuthService(userRepo);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
CreateEventDTO,
|
CreateEventDTO,
|
||||||
UpdateEventDTO,
|
UpdateEventDTO,
|
||||||
EventAction,
|
EventAction,
|
||||||
|
GetMessagesOptions,
|
||||||
} from "@caldav/shared";
|
} from "@caldav/shared";
|
||||||
import { ChatService } from "../services";
|
import { ChatService } from "../services";
|
||||||
import { AuthenticatedRequest } from "../middleware";
|
import { AuthenticatedRequest } from "../middleware";
|
||||||
@@ -66,13 +67,43 @@ export class ChatController {
|
|||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): 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(
|
async getConversation(
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): 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,
|
Conversation,
|
||||||
CreateMessageDTO,
|
CreateMessageDTO,
|
||||||
GetMessagesOptions,
|
GetMessagesOptions,
|
||||||
|
UpdateMessageDTO,
|
||||||
} from "@caldav/shared";
|
} from "@caldav/shared";
|
||||||
import { ChatRepository } from "../../services/interfaces";
|
import { ChatRepository } from "../../services/interfaces";
|
||||||
import { ChatMessageModel, ConversationModel } from "./models";
|
import { ChatMessageModel, ConversationModel } from "./models";
|
||||||
@@ -10,11 +11,15 @@ import { ChatMessageModel, ConversationModel } from "./models";
|
|||||||
export class MongoChatRepository implements ChatRepository {
|
export class MongoChatRepository implements ChatRepository {
|
||||||
// Conversations
|
// Conversations
|
||||||
async getConversationsByUser(userId: string): Promise<Conversation[]> {
|
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> {
|
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)
|
// Messages (cursor-based pagination)
|
||||||
@@ -22,13 +27,44 @@ export class MongoChatRepository implements ChatRepository {
|
|||||||
conversationId: string,
|
conversationId: string,
|
||||||
options?: GetMessagesOptions,
|
options?: GetMessagesOptions,
|
||||||
): Promise<ChatMessage[]> {
|
): 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(
|
async createMessage(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
message: CreateMessageDTO,
|
message: CreateMessageDTO,
|
||||||
): Promise<ChatMessage> {
|
): 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: {
|
proposedChange: {
|
||||||
type: ProposedChangeSchema,
|
type: ProposedChangeSchema,
|
||||||
},
|
},
|
||||||
|
respondedAction: {
|
||||||
|
type: String,
|
||||||
|
enum: ["confirm", "reject"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
CreateEventDTO,
|
CreateEventDTO,
|
||||||
UpdateEventDTO,
|
UpdateEventDTO,
|
||||||
EventAction,
|
EventAction,
|
||||||
|
CreateMessageDTO,
|
||||||
} from "@caldav/shared";
|
} from "@caldav/shared";
|
||||||
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
|
import { ChatRepository, EventRepository, AIProvider } from "./interfaces";
|
||||||
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
|
import { getWeeksOverview, getMonthOverview } from "../utils/eventFormatters";
|
||||||
@@ -252,22 +253,47 @@ export class ChatService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
data: SendMessageDTO,
|
data: SendMessageDTO,
|
||||||
): Promise<ChatResponse> {
|
): Promise<ChatResponse> {
|
||||||
const response = await getTestResponse(
|
let conversationId = data.conversationId;
|
||||||
responseIndex,
|
if (!conversationId) {
|
||||||
this.eventRepo,
|
const conversation = await this.chatRepo.createConversation(userId);
|
||||||
userId,
|
conversationId = conversation.id;
|
||||||
);
|
}
|
||||||
responseIndex++;
|
|
||||||
|
|
||||||
const message: ChatMessage = {
|
// Save user message
|
||||||
id: Date.now().toString(),
|
await this.chatRepo.createMessage(conversationId, {
|
||||||
conversationId: data.conversationId || "temp-conv-id",
|
sender: "user",
|
||||||
|
content: data.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
let response: TestResponse;
|
||||||
|
|
||||||
|
if (process.env.USE_TEST_RESPONSES === "true") {
|
||||||
|
// Test mode: use static responses
|
||||||
|
response = await getTestResponse(responseIndex, this.eventRepo, userId);
|
||||||
|
responseIndex++;
|
||||||
|
} else {
|
||||||
|
// Production mode: use real AI
|
||||||
|
const events = await this.eventRepo.findByUserId(userId);
|
||||||
|
const history = await this.chatRepo.getMessages(conversationId, {
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
response = await this.aiProvider.processMessage(data.content, {
|
||||||
|
userId,
|
||||||
|
conversationHistory: history,
|
||||||
|
existingEvents: events,
|
||||||
|
currentDate: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save and then return assistant response
|
||||||
|
const answerMessage = await this.chatRepo.createMessage(conversationId, {
|
||||||
sender: "assistant",
|
sender: "assistant",
|
||||||
content: response.content,
|
content: response.content,
|
||||||
proposedChange: response.proposedChange,
|
proposedChange: response.proposedChange,
|
||||||
};
|
});
|
||||||
|
|
||||||
return { message, conversationId: message.conversationId };
|
return { message: answerMessage, conversationId: conversationId };
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmEvent(
|
async confirmEvent(
|
||||||
@@ -279,6 +305,12 @@ export class ChatService {
|
|||||||
eventId?: string,
|
eventId?: string,
|
||||||
updates?: UpdateEventDTO,
|
updates?: UpdateEventDTO,
|
||||||
): Promise<ChatResponse> {
|
): Promise<ChatResponse> {
|
||||||
|
// Update original message with respondedAction
|
||||||
|
await this.chatRepo.updateMessage(messageId, {
|
||||||
|
respondedAction: "confirm",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Perform the actual event operation
|
||||||
let content: string;
|
let content: string;
|
||||||
|
|
||||||
if (action === "create" && event) {
|
if (action === "create" && event) {
|
||||||
@@ -298,12 +330,12 @@ export class ChatService {
|
|||||||
content = "Ungültige Aktion.";
|
content = "Ungültige Aktion.";
|
||||||
}
|
}
|
||||||
|
|
||||||
const message: ChatMessage = {
|
// Save response message to DB
|
||||||
id: Date.now().toString(),
|
const message = await this.chatRepo.createMessage(conversationId, {
|
||||||
conversationId,
|
|
||||||
sender: "assistant",
|
sender: "assistant",
|
||||||
content,
|
content,
|
||||||
};
|
});
|
||||||
|
|
||||||
return { message, conversationId };
|
return { message, conversationId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,17 +344,34 @@ export class ChatService {
|
|||||||
conversationId: string,
|
conversationId: string,
|
||||||
messageId: string,
|
messageId: string,
|
||||||
): Promise<ChatResponse> {
|
): Promise<ChatResponse> {
|
||||||
const message: ChatMessage = {
|
// Update original message with respondedAction
|
||||||
id: Date.now().toString(),
|
await this.chatRepo.updateMessage(messageId, { respondedAction: "reject" });
|
||||||
conversationId,
|
|
||||||
|
// Save response message to DB
|
||||||
|
const message = await this.chatRepo.createMessage(conversationId, {
|
||||||
sender: "assistant",
|
sender: "assistant",
|
||||||
content: "Der Vorschlag wurde abgelehnt.",
|
content: "Der Vorschlag wurde abgelehnt.",
|
||||||
};
|
});
|
||||||
|
|
||||||
return { message, conversationId };
|
return { message, conversationId };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConversations(userId: string): Promise<ConversationSummary[]> {
|
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(
|
async getConversation(
|
||||||
@@ -330,6 +379,14 @@ export class ChatService {
|
|||||||
conversationId: string,
|
conversationId: string,
|
||||||
options?: GetMessagesOptions,
|
options?: GetMessagesOptions,
|
||||||
): Promise<ChatMessage[]> {
|
): 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,
|
Conversation,
|
||||||
CreateMessageDTO,
|
CreateMessageDTO,
|
||||||
GetMessagesOptions,
|
GetMessagesOptions,
|
||||||
|
UpdateMessageDTO,
|
||||||
} from "@caldav/shared";
|
} from "@caldav/shared";
|
||||||
|
|
||||||
export interface ChatRepository {
|
export interface ChatRepository {
|
||||||
@@ -15,8 +16,14 @@ export interface ChatRepository {
|
|||||||
conversationId: string,
|
conversationId: string,
|
||||||
options?: GetMessagesOptions,
|
options?: GetMessagesOptions,
|
||||||
): Promise<ChatMessage[]>;
|
): Promise<ChatMessage[]>;
|
||||||
|
|
||||||
createMessage(
|
createMessage(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
message: CreateMessageDTO,
|
message: CreateMessageDTO,
|
||||||
): Promise<ChatMessage>;
|
): Promise<ChatMessage>;
|
||||||
|
|
||||||
|
updateMessage(
|
||||||
|
messageId: string,
|
||||||
|
updates: UpdateMessageDTO,
|
||||||
|
): Promise<ChatMessage | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
43
package-lock.json
generated
43
package-lock.json
generated
@@ -62,13 +62,13 @@
|
|||||||
"name": "@caldav/server",
|
"name": "@caldav/server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.71.2",
|
|
||||||
"@caldav/shared": "*",
|
"@caldav/shared": "*",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mongoose": "^9.1.1",
|
"mongoose": "^9.1.1",
|
||||||
|
"openai": "^6.15.0",
|
||||||
"rrule": "^2.8.1"
|
"rrule": "^2.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -106,26 +106,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@anthropic-ai/sdk": {
|
|
||||||
"version": "0.71.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.71.2.tgz",
|
|
||||||
"integrity": "sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"json-schema-to-ts": "^3.1.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"anthropic-ai-sdk": "bin/cli"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"zod": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
@@ -11252,6 +11232,27 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openai": {
|
||||||
|
"version": "6.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openai/-/openai-6.15.0.tgz",
|
||||||
|
"integrity": "sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"openai": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"zod": "^3.25 || ^4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ws": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
],
|
],
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write \"apps/*/src/**/*.{ts,tsx}\" \"packages/*/src/**/*.ts\""
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export type MessageSender = "user" | "assistant";
|
|||||||
|
|
||||||
export type EventAction = "create" | "update" | "delete";
|
export type EventAction = "create" | "update" | "delete";
|
||||||
|
|
||||||
|
export type RespondedAction = "confirm" | "reject";
|
||||||
|
|
||||||
export interface ProposedEventChange {
|
export interface ProposedEventChange {
|
||||||
action: EventAction;
|
action: EventAction;
|
||||||
eventId?: string; // Required for update/delete
|
eventId?: string; // Required for update/delete
|
||||||
@@ -17,6 +19,7 @@ export interface ChatMessage {
|
|||||||
sender: MessageSender;
|
sender: MessageSender;
|
||||||
content: string;
|
content: string;
|
||||||
proposedChange?: ProposedEventChange;
|
proposedChange?: ProposedEventChange;
|
||||||
|
respondedAction?: RespondedAction;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +46,10 @@ export interface GetMessagesOptions {
|
|||||||
limit?: number; // Default: 20
|
limit?: number; // Default: 20
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateMessageDTO {
|
||||||
|
respondedAction?: RespondedAction;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChatResponse {
|
export interface ChatResponse {
|
||||||
message: ChatMessage;
|
message: ChatMessage;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user