extend chat model with CRUD actions for event changes

- Add ProposedEventChange type with create/update/delete actions
- Replace proposedEvent with proposedChange in ChatMessage
- Add currentDate to AIContext for time-aware AI responses
- Add AI test endpoint for development (/api/ai/test)
- Fix MongoUserRepository type safety with explicit toUser mapping
- Update CLAUDE.md documentation
This commit is contained in:
2026-01-03 19:37:27 +01:00
parent 105a9a4980
commit e553103470
7 changed files with 82 additions and 34 deletions

View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
**CalChat** is a calendar mobile app with AI support. The core concept is creating calendar events through a chat interface with an AI chatbot (Claude). Users can add, edit, and delete events via natural language conversation.
**CalChat** is a calendar mobile app with AI support. The core concept is managing calendar events through a chat interface with an AI chatbot. Users can add, edit, and delete events via natural language conversation.
This is a fullstack TypeScript monorepo with npm workspaces.
@@ -152,6 +152,7 @@ src/
- `GET /api/chat/conversations` - Get all conversations (protected)
- `GET /api/chat/conversations/:id` - Get messages of a conversation with cursor-based pagination (protected)
- `GET /health` - Health check
- `POST /api/ai/test` - AI test endpoint (development only)
### Shared Package (packages/shared)
@@ -163,13 +164,15 @@ src/
├── User.ts # User, CreateUserDTO, LoginDTO, AuthResponse
├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO
└── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
# GetMessagesOptions, ChatResponse, ConversationSummary
# GetMessagesOptions, ChatResponse, ConversationSummary,
# ProposedEventChange, EventAction
```
**Key Types:**
- `User`: id, email, displayName, passwordHash?, createdAt?, updatedAt?
- `CalendarEvent`: id, userId, title, description?, startTime, endTime, note?, isRecurring?, recurrenceRule?
- `ChatMessage`: id, conversationId, sender ('user' | 'assistant'), content, proposedEvent?
- `ChatMessage`: id, conversationId, sender ('user' | 'assistant'), content, proposedChange?
- `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`

View File

@@ -45,6 +45,27 @@ app.get('/health', (_, res) => {
res.json({ status: 'ok' });
});
// AI Test endpoint (for development only)
app.post('/api/ai/test', async (req, res) => {
try {
const { message } = req.body;
if (!message) {
res.status(400).json({ error: 'message is required' });
return;
}
const result = await aiProvider.processMessage(message, {
userId: 'test-user',
conversationHistory: [],
existingEvents: [],
currentDate: new Date(),
});
res.json(result);
} catch (error) {
console.error('AI test error:', error);
res.status(500).json({ error: String(error) });
}
});
// Start server
async function start() {
try {

View File

@@ -1,19 +1,29 @@
import { User } from '@caldav/shared';
import { UserRepository, CreateUserData } from '../../services/interfaces';
import { UserModel } from './models';
import { UserModel, UserDocument } from './models';
export class MongoUserRepository implements UserRepository {
private toUser(doc: UserDocument): User {
return {
id: doc._id.toString(),
email: doc.email,
displayName: doc.displayName,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
}
async findById(id: string): Promise<User | null> {
throw new Error('Not implemented');
}
async findByEmail(email: string): Promise<User | null> {
const user = await UserModel.findOne({ email: email.toLowerCase() });
return user ? user.toJSON() as User : null;
return user ? this.toUser(user) : null;
}
async create(data: CreateUserData): Promise<User> {
const user = await UserModel.create(data);
return user.toJSON() as User;
return this.toUser(user);
}
}

View File

@@ -1,10 +1,10 @@
import mongoose, { Schema, Document } from 'mongoose';
import { ChatMessage, Conversation, CreateEventDTO } from '@caldav/shared';
import { ChatMessage, Conversation, CreateEventDTO, UpdateEventDTO, ProposedEventChange } from '@caldav/shared';
export interface ChatMessageDocument extends Omit<ChatMessage, 'id'>, Document {}
export interface ConversationDocument extends Omit<Conversation, 'id'>, Document {}
const ProposedEventSchema = new Schema<CreateEventDTO>(
const EventSchema = new Schema<CreateEventDTO>(
{
title: { type: String, required: true },
description: { type: String },
@@ -17,6 +17,29 @@ const ProposedEventSchema = new Schema<CreateEventDTO>(
{ _id: false }
);
const UpdatesSchema = new Schema<UpdateEventDTO>(
{
title: { type: String },
description: { type: String },
startTime: { type: Date },
endTime: { type: Date },
note: { type: String },
isRecurring: { type: Boolean },
recurrenceRule: { type: String },
},
{ _id: false }
);
const ProposedChangeSchema = new Schema<ProposedEventChange>(
{
action: { type: String, enum: ['create', 'update', 'delete'], required: true },
eventId: { type: String },
event: { type: EventSchema },
updates: { type: UpdatesSchema },
},
{ _id: false }
);
const ChatMessageSchema = new Schema<ChatMessageDocument>(
{
conversationId: {
@@ -32,8 +55,8 @@ const ChatMessageSchema = new Schema<ChatMessageDocument>(
type: String,
required: true,
},
proposedEvent: {
type: ProposedEventSchema,
proposedChange: {
type: ProposedChangeSchema,
},
},
{

View File

@@ -1,14 +1,15 @@
import { CalendarEvent, ChatMessage, CreateEventDTO } from '@caldav/shared';
import { CalendarEvent, ChatMessage, ProposedEventChange } from '@caldav/shared';
export interface AIContext {
userId: string;
conversationHistory: ChatMessage[];
existingEvents: CalendarEvent[];
currentDate: Date;
}
export interface AIResponse {
content: string;
proposedEvent?: CreateEventDTO;
proposedChange?: ProposedEventChange;
}
export interface AIProvider {

19
package-lock.json generated
View File

@@ -9568,19 +9568,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -14318,12 +14305,6 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"license": "MIT"
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",

View File

@@ -1,13 +1,22 @@
import { CreateEventDTO } from './CalendarEvent';
import { CreateEventDTO, UpdateEventDTO } from './CalendarEvent';
export type MessageSender = 'user' | 'assistant';
export type EventAction = 'create' | 'update' | 'delete';
export interface ProposedEventChange {
action: EventAction;
eventId?: string; // Required for update/delete
event?: CreateEventDTO; // Required for create
updates?: UpdateEventDTO; // Required for update
}
export interface ChatMessage {
id: string;
conversationId: string;
sender: MessageSender;
content: string;
proposedEvent?: CreateEventDTO;
proposedChange?: ProposedEventChange;
createdAt?: Date;
}
@@ -26,7 +35,7 @@ export interface SendMessageDTO {
export interface CreateMessageDTO {
sender: MessageSender;
content: string;
proposedEvent?: CreateEventDTO;
proposedChange?: ProposedEventChange;
}
export interface GetMessagesOptions {