diff --git a/CALENDAR_ENHANCEMENT.md b/CALENDAR_ENHANCEMENT.md new file mode 100644 index 00000000..0ffa4f49 --- /dev/null +++ b/CALENDAR_ENHANCEMENT.md @@ -0,0 +1,169 @@ +# Calendar UI Enhancement with Place/Time Context + +## Overview + +This enhancement adds comprehensive place and time context management to the QCX calendar system, integrating with Mapbox MCP to provide structured location and temporal information. + +## Features + +### 1. Enhanced Time Management +- **Time Picker**: Select specific time (HH:MM:SS) for calendar notes +- **Timezone Support**: Choose from 20+ common timezones +- **Auto-detection**: Automatically detects user's current timezone + +### 2. Location Tagging with Mapbox MCP +- **Map Integration**: Tag notes with current map position +- **Geocoding**: Automatically fetches place names using Mapbox MCP +- **Visual Feedback**: Shows tagged coordinates in real-time + +### 3. Structured Context Format +All location-tagged notes display context in the standardized format: +``` +latitude, longitude TIME (HH:MM:SS): DD/MM/YYYY Timezone +``` + +Example: +``` +40.758896, -73.985130 TIME (14:30:45): 20/11/2025 America/New_York +``` + +## Implementation Details + +### Database Schema Changes + +Added `timezone` field to `calendar_notes` table: +```sql +ALTER TABLE calendar_notes ADD COLUMN timezone VARCHAR(100); +``` + +### New Components + +1. **CalendarNotepadEnhanced** (`components/calendar-notepad-enhanced.tsx`) + - Enhanced version of the calendar notepad + - Includes time and timezone selectors + - Integrates with Mapbox MCP for geocoding + +2. **Calendar Context Utilities** (`lib/utils/calendar-context.ts`) + - `formatPlaceTimeContext()`: Formats notes into standardized context string + - `parsePlaceTimeContext()`: Parses context strings back into components + - `createGeoJSONPoint()`: Creates GeoJSON Point objects + - `getCurrentTimezone()`: Gets user's current timezone + - `COMMON_TIMEZONES`: List of 20+ common timezones + +### Updated Files + +- `lib/db/schema.ts`: Added timezone field +- `lib/types/index.ts`: Updated CalendarNote type +- `components/chat.tsx`: Switched to CalendarNotepadEnhanced +- `drizzle/migrations/0002_add_timezone_to_calendar_notes.sql`: Migration file + +## Usage + +### Creating a Note with Location and Time + +1. Open the calendar view in QCX +2. Select the desired date from the date picker +3. Set the time using the time picker (defaults to current time) +4. Select the appropriate timezone +5. Navigate the map to your desired location +6. Click the MapPin icon to tag the location +7. Type your note content +8. Press Cmd+Enter (Mac) or Ctrl+Enter (Windows) to save + +### Viewing Notes + +Notes with location tags display: +- The note content +- A formatted context string with coordinates, time, date, and timezone +- A MapPin button to fly to the location on the map + +### Mapbox MCP Integration + +The calendar automatically integrates with Mapbox MCP when available: +- **Connected**: Automatically geocodes coordinates to place names +- **Disconnected**: Falls back to simple coordinate tagging + +## API + +### formatPlaceTimeContext(note: CalendarNote): string + +Formats a calendar note into the standardized context string. + +**Parameters:** +- `note`: CalendarNote object with locationTags + +**Returns:** +- Formatted string: `"lat, lng TIME (HH:MM:SS): DD/MM/YYYY Timezone"` +- Empty string if no location tags + +### parsePlaceTimeContext(contextString: string): object | null + +Parses a context string back into components. + +**Parameters:** +- `contextString`: Formatted context string + +**Returns:** +```typescript +{ + latitude: number + longitude: number + time: string // HH:MM:SS + date: string // DD/MM/YYYY + timezone: string +} +``` + +## Supported Timezones + +The system supports 20+ common timezones including: +- UTC +- Americas: New York, Chicago, Denver, Los Angeles, Toronto, Mexico City, SĆ£o Paulo +- Europe: London, Paris, Berlin, Moscow +- Asia: Dubai, Kolkata, Shanghai, Tokyo, Seoul, Singapore +- Oceania: Sydney, Auckland + +## Migration + +To apply the database changes: + +```bash +bun run db:migrate +``` + +Or manually run the migration: +```bash +psql -d your_database -f drizzle/migrations/0002_add_timezone_to_calendar_notes.sql +``` + +## Testing + +The enhanced calendar can be tested by: +1. Creating notes with different times and timezones +2. Tagging locations from the map +3. Verifying the context string format +4. Flying to tagged locations +5. Testing with and without Mapbox MCP connection + +## Future Enhancements + +Potential improvements: +- Recurring events support +- Calendar export (iCal format) +- Time range queries +- Timezone conversion display +- Location-based note search +- Integration with external calendar services + +## Dependencies + +- Mapbox MCP server (optional, for enhanced geocoding) +- Mapbox GL JS (for map integration) +- Lucide React (for icons) +- Drizzle ORM (for database) + +## Compatibility + +- Works with existing calendar notes (timezone defaults to UTC) +- Backward compatible with original CalendarNotepad +- Mobile and desktop responsive diff --git a/components/calendar-notepad-enhanced.tsx b/components/calendar-notepad-enhanced.tsx new file mode 100644 index 00000000..646c60b4 --- /dev/null +++ b/components/calendar-notepad-enhanced.tsx @@ -0,0 +1,278 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { ChevronLeft, ChevronRight, MapPin, Clock, Globe } from 'lucide-react' +import { cn } from '@/lib/utils' +import { getNotes, saveNote } from '@/lib/actions/calendar' +import type { CalendarNote, NewCalendarNote } from '@/lib/types' +import { useMapData } from '@/components/map/map-data-context' +import { useMCPMapClient } from '@/mapbox_mcp/hooks' +import { formatPlaceTimeContext, COMMON_TIMEZONES, getCurrentTimezone } from '@/lib/utils/calendar-context' + +interface CalendarNotepadEnhancedProps { + chatId?: string +} + +export function CalendarNotepadEnhanced({ chatId }: CalendarNotepadEnhancedProps) { + const [selectedDate, setSelectedDate] = useState(new Date()) + const [dateOffset, setDateOffset] = useState(0) + const [notes, setNotes] = useState([]) + const [noteContent, setNoteContent] = useState('') + const [taggedLocation, setTaggedLocation] = useState(null) + const [selectedTime, setSelectedTime] = useState('') + const [selectedTimezone, setSelectedTimezone] = useState(getCurrentTimezone()) + const [isLoadingLocation, setIsLoadingLocation] = useState(false) + + const { mapData, setMapData } = useMapData() + const { geocodeLocation, isConnected } = useMCPMapClient() + + useEffect(() => { + const fetchNotes = async () => { + const fetchedNotes = await getNotes(selectedDate, chatId ?? null) + setNotes(fetchedNotes) + } + fetchNotes() + }, [selectedDate, chatId]) + + useEffect(() => { + // Set current time as default + const now = new Date() + const hours = String(now.getHours()).padStart(2, '0') + const minutes = String(now.getMinutes()).padStart(2, '0') + const seconds = String(now.getSeconds()).padStart(2, '0') + setSelectedTime(`${hours}:${minutes}:${seconds}`) + }, []) + + const generateDateRange = (offset: number) => { + const dates = [] + const today = new Date() + for (let i = 0; i < 7; i++) { + const date = new Date(today) + date.setDate(today.getDate() + offset + i) + dates.push(date) + } + return dates + } + + const dateRange = generateDateRange(dateOffset) + + const isSameDay = (date1: Date, date2: Date) => { + return ( + date1.getDate() === date2.getDate() && + date1.getMonth() === date2.getMonth() && + date1.getFullYear() === date2.getFullYear() + ) + } + + + + const handleAddNote = async (e: React.KeyboardEvent) => { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + if (!noteContent.trim()) return + + // Parse the selected time and combine with selected date + const [hours, minutes, seconds] = selectedTime.split(':').map(Number) + const noteDate = new Date(selectedDate) + noteDate.setHours(hours, minutes, seconds || 0) + + const newNote: NewCalendarNote = { + date: noteDate, + content: noteContent, + chatId: chatId ?? null, + userId: '', // This will be set on the server + locationTags: taggedLocation, + userTags: null, + mapFeatureId: null, + timezone: selectedTimezone, + } + + const savedNote = await saveNote(newNote) + if (savedNote) { + setNotes([savedNote, ...notes]) + setNoteContent("") + setTaggedLocation(null) + } + } + } + + const handleTagLocation = async () => { + if (mapData.targetPosition) { + // Convert LngLatLike to [lng, lat] array + let coordinates: [number, number] + if (Array.isArray(mapData.targetPosition)) { + coordinates = mapData.targetPosition as [number, number] + } else if ('lng' in mapData.targetPosition && 'lat' in mapData.targetPosition) { + coordinates = [mapData.targetPosition.lng, mapData.targetPosition.lat] + } else if ('lon' in mapData.targetPosition && 'lat' in mapData.targetPosition) { + coordinates = [mapData.targetPosition.lon, mapData.targetPosition.lat] + } else { + console.error('Invalid targetPosition format') + return + } + + setTaggedLocation({ + type: 'Point', + coordinates: coordinates + }) + + // Try to get place name using Mapbox MCP if connected + if (isConnected) { + setIsLoadingLocation(true) + try { + const [lng, lat] = coordinates + const result = await geocodeLocation(`${lng},${lat}`) + if (result && result.location && result.location.place_name) { + setNoteContent(prev => `${prev}\nšŸ“ ${result.location.place_name}`) + } + } catch (error) { + console.error('Error geocoding location:', error) + } finally { + setIsLoadingLocation(false) + } + } else { + setNoteContent(prev => `${prev} #location`) + } + } + } + + const handleFlyTo = (location: any) => { + if (location && location.coordinates) { + setMapData(prev => ({ ...prev, targetPosition: location.coordinates })) + } + } + + const timezones = COMMON_TIMEZONES + + return ( +
+
+ +
+ {dateRange.map((date) => ( + + ))} +
+ +
+ + {/* Time and Timezone Selection */} +
+
+ + setSelectedTime(e.target.value)} + className="flex-1 p-2 bg-input rounded-md border focus:ring-ring focus:ring-2 focus:outline-none text-sm" + /> +
+
+ + +
+
+ +
+
+