diff --git a/CLAUDE.md b/CLAUDE.md index baee65e..0bd8d51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 5b6c368..b1a5ea4 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -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 { diff --git a/apps/server/src/repositories/mongo/MongoUserRepository.ts b/apps/server/src/repositories/mongo/MongoUserRepository.ts index 11b19bd..6e7fd46 100644 --- a/apps/server/src/repositories/mongo/MongoUserRepository.ts +++ b/apps/server/src/repositories/mongo/MongoUserRepository.ts @@ -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 { throw new Error('Not implemented'); } async findByEmail(email: string): Promise { 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 { const user = await UserModel.create(data); - return user.toJSON() as User; + return this.toUser(user); } } diff --git a/apps/server/src/repositories/mongo/models/ChatModel.ts b/apps/server/src/repositories/mongo/models/ChatModel.ts index 5e9841d..c4f4304 100644 --- a/apps/server/src/repositories/mongo/models/ChatModel.ts +++ b/apps/server/src/repositories/mongo/models/ChatModel.ts @@ -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, Document {} export interface ConversationDocument extends Omit, Document {} -const ProposedEventSchema = new Schema( +const EventSchema = new Schema( { title: { type: String, required: true }, description: { type: String }, @@ -17,6 +17,29 @@ const ProposedEventSchema = new Schema( { _id: false } ); +const UpdatesSchema = new Schema( + { + 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( + { + action: { type: String, enum: ['create', 'update', 'delete'], required: true }, + eventId: { type: String }, + event: { type: EventSchema }, + updates: { type: UpdatesSchema }, + }, + { _id: false } +); + const ChatMessageSchema = new Schema( { conversationId: { @@ -32,8 +55,8 @@ const ChatMessageSchema = new Schema( type: String, required: true, }, - proposedEvent: { - type: ProposedEventSchema, + proposedChange: { + type: ProposedChangeSchema, }, }, { diff --git a/apps/server/src/services/interfaces/AIProvider.ts b/apps/server/src/services/interfaces/AIProvider.ts index f3a8d0d..87337d1 100644 --- a/apps/server/src/services/interfaces/AIProvider.ts +++ b/apps/server/src/services/interfaces/AIProvider.ts @@ -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 { diff --git a/package-lock.json b/package-lock.json index 68f5aa9..8bfae17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/shared/src/models/ChatMessage.ts b/packages/shared/src/models/ChatMessage.ts index 2ddec96..6d138db 100644 --- a/packages/shared/src/models/ChatMessage.ts +++ b/packages/shared/src/models/ChatMessage.ts @@ -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 {