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:
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## 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.
|
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` - Get all conversations (protected)
|
||||||
- `GET /api/chat/conversations/:id` - Get messages of a conversation with cursor-based pagination (protected)
|
- `GET /api/chat/conversations/:id` - Get messages of a conversation with cursor-based pagination (protected)
|
||||||
- `GET /health` - Health check
|
- `GET /health` - Health check
|
||||||
|
- `POST /api/ai/test` - AI test endpoint (development only)
|
||||||
|
|
||||||
### Shared Package (packages/shared)
|
### Shared Package (packages/shared)
|
||||||
|
|
||||||
@@ -163,13 +164,15 @@ src/
|
|||||||
├── User.ts # User, CreateUserDTO, LoginDTO, AuthResponse
|
├── User.ts # User, CreateUserDTO, LoginDTO, AuthResponse
|
||||||
├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO
|
├── CalendarEvent.ts # CalendarEvent, CreateEventDTO, UpdateEventDTO
|
||||||
└── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
|
└── ChatMessage.ts # ChatMessage, Conversation, SendMessageDTO, CreateMessageDTO,
|
||||||
# GetMessagesOptions, ChatResponse, ConversationSummary
|
# GetMessagesOptions, ChatResponse, ConversationSummary,
|
||||||
|
# ProposedEventChange, EventAction
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Types:**
|
**Key Types:**
|
||||||
- `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?
|
||||||
- `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)
|
- `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`
|
||||||
|
|||||||
@@ -45,6 +45,27 @@ app.get('/health', (_, res) => {
|
|||||||
res.json({ status: 'ok' });
|
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
|
// Start server
|
||||||
async function start() {
|
async function start() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import { User } from '@caldav/shared';
|
import { User } from '@caldav/shared';
|
||||||
import { UserRepository, CreateUserData } from '../../services/interfaces';
|
import { UserRepository, CreateUserData } from '../../services/interfaces';
|
||||||
import { UserModel } from './models';
|
import { UserModel, UserDocument } from './models';
|
||||||
|
|
||||||
export class MongoUserRepository implements UserRepository {
|
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> {
|
async findById(id: string): Promise<User | null> {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByEmail(email: string): Promise<User | null> {
|
async findByEmail(email: string): Promise<User | null> {
|
||||||
const user = await UserModel.findOne({ email: email.toLowerCase() });
|
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> {
|
async create(data: CreateUserData): Promise<User> {
|
||||||
const user = await UserModel.create(data);
|
const user = await UserModel.create(data);
|
||||||
return user.toJSON() as User;
|
return this.toUser(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
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 ChatMessageDocument extends Omit<ChatMessage, 'id'>, Document {}
|
||||||
export interface ConversationDocument extends Omit<Conversation, '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 },
|
title: { type: String, required: true },
|
||||||
description: { type: String },
|
description: { type: String },
|
||||||
@@ -17,6 +17,29 @@ const ProposedEventSchema = new Schema<CreateEventDTO>(
|
|||||||
{ _id: false }
|
{ _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>(
|
const ChatMessageSchema = new Schema<ChatMessageDocument>(
|
||||||
{
|
{
|
||||||
conversationId: {
|
conversationId: {
|
||||||
@@ -32,8 +55,8 @@ const ChatMessageSchema = new Schema<ChatMessageDocument>(
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
proposedEvent: {
|
proposedChange: {
|
||||||
type: ProposedEventSchema,
|
type: ProposedChangeSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { CalendarEvent, ChatMessage, CreateEventDTO } from '@caldav/shared';
|
import { CalendarEvent, ChatMessage, ProposedEventChange } from '@caldav/shared';
|
||||||
|
|
||||||
export interface AIContext {
|
export interface AIContext {
|
||||||
userId: string;
|
userId: string;
|
||||||
conversationHistory: ChatMessage[];
|
conversationHistory: ChatMessage[];
|
||||||
existingEvents: CalendarEvent[];
|
existingEvents: CalendarEvent[];
|
||||||
|
currentDate: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIResponse {
|
export interface AIResponse {
|
||||||
content: string;
|
content: string;
|
||||||
proposedEvent?: CreateEventDTO;
|
proposedChange?: ProposedEventChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIProvider {
|
export interface AIProvider {
|
||||||
|
|||||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -9568,19 +9568,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/json-schema-traverse": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||||
@@ -14318,12 +14305,6 @@
|
|||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { CreateEventDTO } from './CalendarEvent';
|
import { CreateEventDTO, UpdateEventDTO } from './CalendarEvent';
|
||||||
|
|
||||||
export type MessageSender = 'user' | 'assistant';
|
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 {
|
export interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
sender: MessageSender;
|
sender: MessageSender;
|
||||||
content: string;
|
content: string;
|
||||||
proposedEvent?: CreateEventDTO;
|
proposedChange?: ProposedEventChange;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +35,7 @@ export interface SendMessageDTO {
|
|||||||
export interface CreateMessageDTO {
|
export interface CreateMessageDTO {
|
||||||
sender: MessageSender;
|
sender: MessageSender;
|
||||||
content: string;
|
content: string;
|
||||||
proposedEvent?: CreateEventDTO;
|
proposedChange?: ProposedEventChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetMessagesOptions {
|
export interface GetMessagesOptions {
|
||||||
|
|||||||
Reference in New Issue
Block a user