Skip to content

Calendar Icon #636

@ngoiyaeric

Description

@ngoiyaeric

Calendar Icon:

This is a calendar centric notepad for users to add time sensitive context regarding their geospatial workflows whilst collaborating with other users. Users may move dates from a top scroll bar into future or past dates retrieving notes or adding reminders for a future event.
Users may tag locations or tag their collaborators.

Information is indexed asynchronously to the language model into the context.
It should be a minimal design that retains information and enhances knowledge discovery.

Example:
User is indexing a plot for a commercial property with collaborators in Norway.
The collaborators are working asynchronously in the map render and prompting the system for discovery. The users have ragged additional documents. The users realize they need to revisit the site in a week when the local Municipal has performed their land divisions. They input this into the notepad with their survey requirements, tagging each other and the plot as well as this event’s future date.

The system understands and retains context and seeks additional knowledge asynchronously.

I need to clarify what you're asking about. Based on the codebase context I have access to, you appear to be describing a feature request or design specification for a calendar-based notepad component that doesn't currently exist in the QCX codebase.

Current State of Calendar Icon

The calendar icon (CalendarDays) currently exists in the UI but has no functionality implemented:

  • In the desktop header at components/header.tsx:38-40, there's a CalendarDays button with no click handler 1
  • In the mobile icons bar at components/mobile-icons-bar.tsx:43-45, the same icon appears without any associated functionality 2

What Doesn't Exist Yet

The features you're describing are not implemented in the current codebase:

  1. Calendar-centric notepad interface - No component exists for this
  2. Date scrolling/navigation - Not present
  3. Note creation with location/collaborator tagging - Not implemented
  4. Asynchronous indexing to language model - No such system exists
  5. Context retention for geospatial workflows - Not built

What Would Need to Be Built

To implement your specification, you would need to create:

  1. A new calendar component that renders when the CalendarDays icon is clicked
  2. A data model for storing notes with timestamps, location tags, and user tags
  3. Integration with the existing MapDataContext to link notes to map features 3
  4. A persistence layer (likely extending the existing Supabase schema)
  5. An indexing mechanism to feed note content into the AI context

Notes

This appears to be a feature specification rather than a question about existing code. The QCX application currently focuses on AI-powered geospatial chat with map integration, but does not have any calendar or time-based note-taking functionality. If you're looking to implement this feature, I'd be happy to help design the architecture or discuss how it could integrate with the existing map and AI systems.

Wiki pages you might want to explore:

Citations

File: components/header.tsx (L38-40)

        <Button variant="ghost" size="icon">
          <CalendarDays className="h-[1.2rem] w-[1.2rem]" />
        </Button>

File: components/mobile-icons-bar.tsx (L43-45)

      <Button variant="ghost" size="icon">
        <CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
      </Button>

File: components/map/map-query-handler.tsx (L5-5)

import { useMapData } from './map-data-context'; 

Implementation Overview: Calendar Notepad System

Based on your specification, you're asking how to implement a calendar-centric notepad feature that integrates with QCX's existing geospatial chat and map system.

System Architecture

The calendar notepad would connect to three core systems in QCX:

  1. Map System - For location tagging and spatial context
  2. Chat/Message System - For persistence and AI context
  3. AI Agent System - For asynchronous knowledge indexing 1 2

Implementation Components

1. Calendar UI Component

Create a new component components/calendar-notepad.tsx that renders when the CalendarDays icon is clicked. 3

Key features:

  • Date navigation bar (horizontal scroll)
  • Note creation/editing interface
  • Tag input for locations and collaborators
  • Integration with existing UI patterns
interface CalendarNote {
  id: string;
  userId: string;
  date: Date;
  content: string;
  locationTags: [number, number][]; // [lng, lat] coordinates
  userTags: string[]; // User IDs
  mapFeatureId?: string; // Link to drawn features
  createdAt: Date;
  updatedAt: Date;
}

2. Database Schema Extension

Extend the existing Supabase schema to store calendar notes. The current system uses Drizzle ORM with chats and messages tables. 4

Add a new table:

CREATE TABLE calendar_notes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id),
  chat_id UUID REFERENCES chats(id), -- Link to chat context
  date TIMESTAMP NOT NULL,
  content TEXT NOT NULL,
  location_tags JSONB, -- Array of coordinates
  user_tags TEXT[], -- Array of user IDs
  map_feature_id TEXT, -- Reference to drawn features
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

3. Context Integration with MapDataContext

The calendar notes need to interact with the map system through MapDataContext. 1

Integration points:

  • Location tagging: When a user tags a location, capture mapData.targetPosition or mapData.drawnFeatures
  • Feature linking: Store references to drawn polygons/lines from the map
  • Bidirectional updates: Clicking a calendar note should update setMapData() to navigate to tagged locations 5

4. AI Context Indexing

The system already has a pattern for feeding context to AI agents through the chat system. 6

Implementation approach:

  1. Store notes as special message types: Similar to how updateDrawingContext stores drawn features as messages with role: 'data' 7

  2. Create a calendar context message type:

const calendarContextMessage: Omit<DbNewMessage, 'chatId'> = {
  userId: userId,
  role: 'data',
  content: JSON.stringify({
    type: 'calendar_note',
    note: calendarNote,
    date: note.date,
    locationTags: note.locationTags,
    userTags: note.userTags
  }),
  createdAt: new Date()
};
  1. Asynchronous indexing: When notes are created/updated, append them to the chat's message history so the AI agents can access them in future queries 8

5. Geospatial Tool Integration

The existing geospatialQueryTool can be extended to understand calendar context. 9

When the AI processes a query, it would:

  1. Check for relevant calendar notes based on date/location
  2. Include note content in the context window
  3. Use MapQueryHandler to navigate to tagged locations 10

6. Collaboration Features

For user tagging and collaboration:

  1. User mentions: Parse @username syntax in note content
  2. Notifications: Create a notification system (new table) when users are tagged
  3. Shared context: Notes are associated with chatId, so collaborators in the same chat see the same notes
  4. Real-time updates: Use Supabase real-time subscriptions to sync notes across users

Implementation Flow

graph TB
    User["User clicks CalendarDays icon"]
    CalendarUI["Calendar Notepad Component"]
    CreateNote["Create/Edit Note"]
    TagLocation["Tag Location from Map"]
    TagUsers["Tag Collaborators"]
    SaveNote["Save to Database"]
    CreateMessage["Create Calendar Context Message"]
    AIIndex["AI Agent Indexes Content"]
    MapUpdate["Update Map View"]
    
    User --> CalendarUI
    CalendarUI --> CreateNote
    CreateNote --> TagLocation
    CreateNote --> TagUsers
    TagLocation --> MapUpdate
    CreateNote --> SaveNote
    SaveNote --> CreateMessage
    CreateMessage --> AIIndex
    
    MapUpdate -.->|"setMapData()"| MapDataContext["MapDataContext"]
    TagLocation -.->|"reads"| MapDataContext
Loading

Key Integration Points

  1. Map Context: Use useMapData() hook to read/write map state 11

  2. Chat Persistence: Use saveChat() and createMessage() functions 12

  3. AI State: Notes become part of AIState messages array, accessible to all agents 13

  4. Map Features: Link to drawnFeatures from MapDataContext 5

Notes

The implementation leverages existing patterns in QCX:

  • The MapDataContext already handles bidirectional map-chat communication
  • The message system with role: 'data' provides a precedent for storing structured context
  • The MapQueryHandler pattern shows how to trigger map updates from AI responses

The calendar notepad would be a new top-level feature but integrates naturally with the existing architecture. The main new components are the UI layer and database schema; the integration points already exist in the codebase.

Wiki pages you might want to explore:

Citations

File: lib/actions/chat-db.ts (L74-148)

/**
 * Saves a chat and its messages. If the chat exists, it updates it.
 * This function should handle both creating new chats and appending messages.
 * The PR implies complex logic for saving, including message IDs.
 * This is a simplified version; PR #533 might have more granular message saving.
 * @param chatData - The chat data to save.
 * @param messagesData - An array of messages to save with the chat.
 * @returns The saved chat ID.
 */
export async function saveChat(chatData: NewChat, messagesData: Omit<NewMessage, 'chatId'>[]): Promise<string | null> {
  if (!chatData.userId) {
    console.error('Cannot save chat without a userId');
    return null;
  }

  // Transaction to ensure atomicity
  return db.transaction(async (tx) => {
    let chatId = chatData.id;

    if (chatId) { // If chat ID is provided, assume update or append messages
      const existingChat = await tx.select({ id: chats.id }).from(chats).where(eq(chats.id, chatId)).limit(1);
      if (!existingChat.length) {
        // Chat doesn't exist, so create it
        const newChatResult = await tx.insert(chats).values(chatData).returning({ id: chats.id });
        chatId = newChatResult[0].id;
      } else {
        // Optionally update chat metadata here if needed, e.g., title
        if (chatData.title) {
          await tx.update(chats).set({ title: chatData.title }).where(eq(chats.id, chatId));
        }
      }
    } else { // No chat ID, create new chat
      const newChatResult = await tx.insert(chats).values(chatData).returning({ id: chats.id });
      chatId = newChatResult[0].id;
    }

    if (!chatId) {
      // console.error('Failed to establish chatId within transaction.'); // Optional: for server logs
      throw new Error('Failed to establish chatId for chat operation.');
    }

    // Save messages
    if (messagesData && messagesData.length > 0) {
      const messagesToInsert = messagesData.map(msg => ({
        ...msg,
        chatId: chatId!, // Ensure chatId is set for all messages
        userId: msg.userId || chatData.userId!, // Ensure userId is set
      }));
      await tx.insert(messages).values(messagesToInsert);
    }
    return chatId;
  });
}


/**
 * Creates a single message within a chat.
 * PR #533 has commits like "feat: Add message update and trailing deletion logic",
 * suggesting more granular message operations. This is a basic create.
 * @param messageData - The message data to save.
 * @returns The created message object or null if error.
 */
export async function createMessage(messageData: NewMessage): Promise<Message | null> {
  if (!messageData.chatId || !messageData.userId || !messageData.role || !messageData.content) {
    console.error('Missing required fields for creating a message.');
    return null;
  }
  try {
    const result = await db.insert(messages).values(messageData).returning();
    return result[0] || null;
  } catch (error) {
    console.error('Error creating message:', error);
    return null;
  }
}

File: components/header.tsx (L38-40)

        <Button variant="ghost" size="icon">
          <CalendarDays className="h-[1.2rem] w-[1.2rem]" />
        </Button>

File: components/chat.tsx (L74-83)

  // Get mapData to access drawnFeatures
  const { mapData } = useMapData();

  // useEffect to call the server action when drawnFeatures changes
  useEffect(() => {
    if (id && mapData.drawnFeatures && mapData.drawnFeatures.length > 0) {
      console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures);
      updateDrawingContext(id, mapData.drawnFeatures);
    }
  }, [id, mapData.drawnFeatures]);

File: app/actions.tsx (L1-29)

import {
  StreamableValue,
  createAI,
  createStreamableUI,
  createStreamableValue,
  getAIState,
  getMutableAIState
} from 'ai/rsc'
import { CoreMessage, ToolResultPart } from 'ai'
import { nanoid } from 'nanoid'
import type { FeatureCollection } from 'geojson'
import { Spinner } from '@/components/ui/spinner'
import { Section } from '@/components/section'
import { FollowupPanel } from '@/components/followup-panel'
import { inquire, researcher, taskManager, querySuggestor, resolutionSearch } from '@/lib/agents'
// Removed import of useGeospatialToolMcp as it no longer exists and was incorrectly used here.
// The geospatialTool (if used by agents like researcher) now manages its own MCP client.
import { writer } from '@/lib/agents/writer'
import { saveChat, getSystemPrompt } from '@/lib/actions/chat' // Added getSystemPrompt
import { Chat, AIMessage } from '@/lib/types'
import { UserMessage } from '@/components/user-message'
import { BotMessage } from '@/components/message'
import { SearchSection } from '@/components/search-section'
import SearchRelated from '@/components/search-related'
import { GeoJsonLayer } from '@/components/map/geojson-layer'
import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { MapQueryHandler } from '@/components/map/map-query-handler' // Add this import

File: app/actions.tsx (L640-655)

        case 'tool':
          try {
            const toolOutput = JSON.parse(content as string)
            const isCollapsed = createStreamableValue()
            isCollapsed.done(true)

            if (
              toolOutput.type === 'MAP_QUERY_TRIGGER' &&
              name === 'geospatialQueryTool'
            ) {
              return {
                id,
                component: <MapQueryHandler toolOutput={toolOutput} />,
                isCollapsed: false
              }
            }

File: lib/actions/chat.ts (L165-205)

export async function updateDrawingContext(chatId: string, drawnFeatures: any[]) {
  'use server';
  console.log('[Action] updateDrawingContext called for chatId:', chatId);

  const userId = await getCurrentUserIdOnServer(); // Essential for creating a user-associated message
  if (!userId) {
    console.error('updateDrawingContext: Could not get current user ID. User must be authenticated.');
    return { error: 'User not authenticated' };
  }

  // The old version fetched the whole chat. Now we just create a new message.
  // The AIMessage type might be from '@/lib/types' and need mapping to DbNewMessage
  const newDrawingMessage: Omit<DbNewMessage, 'chatId'> = {
    // id: `drawnData-${Date.now().toString()}`, // Let DB generate UUID
    userId: userId,
    role: 'data' as 'user' | 'assistant' | 'system' | 'tool' | 'data', // Cast 'data' if not in standard roles
    content: JSON.stringify(drawnFeatures), // Store features as stringified JSON
    // type: 'drawing_context', // This field is not in the Drizzle 'messages' schema.
    // If `type` is important, the schema needs to be updated or content needs to reflect it.
    // For now, we'll assume 'content' holds the necessary info and role='data' signifies it.
    createdAt: new Date(),
  };

  try {
    // We need to ensure the message is associated with the chat.
    // dbCreateMessage requires chatId.
    const messageToSave: DbNewMessage = {
      ...newDrawingMessage,
      chatId: chatId,
    };
    const savedMessage = await dbCreateMessage(messageToSave);
    if (!savedMessage) {
      throw new Error('Failed to save drawing context message.');
    }
    console.log('Drawing context message added to chat:', chatId, 'messageId:', savedMessage.id);
    return { success: true, messageId: savedMessage.id };
  } catch (error) {
    console.error('updateDrawingContext: Error saving drawing context message:', error);
    return { error: 'Failed to save drawing context message' };
  }
}

File: components/map/map-query-handler.tsx (L30-74)

export const MapQueryHandler: React.FC<MapQueryHandlerProps> = ({ toolOutput }) => {
  const { setMapData } = useMapData();

  useEffect(() => {
    if (toolOutput && toolOutput.mcp_response && toolOutput.mcp_response.location) {
      const { latitude, longitude, place_name } = toolOutput.mcp_response.location;

      if (typeof latitude === 'number' && typeof longitude === 'number') {
        console.log(`MapQueryHandler: Received data from geospatialTool. Place: ${place_name}, Lat: ${latitude}, Lng: ${longitude}`);
        setMapData(prevData => ({
          ...prevData,
          // Ensure coordinates are in [lng, lat] format for MapboxGL
          targetPosition: [longitude, latitude], 
          // Optionally store more info from mcp_response if needed by MapboxMap component later
          mapFeature: { 
            place_name, 
            // Potentially add mapUrl or other details from toolOutput.mcp_response
            mapUrl: toolOutput.mcp_response?.mapUrl 
          } 
        }));
      } else {
        console.warn("MapQueryHandler: Invalid latitude/longitude in toolOutput.mcp_response:", toolOutput.mcp_response.location);
        // Clear target position if data is invalid
        setMapData(prevData => ({
          ...prevData,
          targetPosition: null,
          mapFeature: null
        }));
      }
    } else {
      // This case handles when toolOutput or its critical parts are missing.
      // Depending on requirements, could fall back to originalUserInput and useMCPMapClient,
      // or simply log that no valid data was provided from the tool.
      // For this subtask, we primarily focus on using the new toolOutput.
      if (toolOutput) { // It exists, but data is not as expected
        console.warn("MapQueryHandler: toolOutput provided, but mcp_response or location data is missing.", toolOutput);
      }
      // If toolOutput is null/undefined, this component might not need to do anything,
      // or it's an indication that it shouldn't have been rendered/triggered.
      // For now, if no valid toolOutput, we clear map data or leave it as is.
      // setMapData(prevData => ({ ...prevData, targetPosition: null, mapFeature: null }));
    }
    // The dependencies for this useEffect should be based on the props that trigger its logic.
    // If originalUserInput and the old MCP client were still used as a fallback, they'd be dependencies.
  }, [toolOutput, setMapData]);

File: components/map/mapbox-map.tsx (L34-34)

  const { mapData, setMapData } = useMapData(); // Consume the new context, get setMapData

File: app/search/[id]/page.tsx (L44-60)

  // Fetch messages for the chat
  const dbMessages: DrizzleMessage[] = await getChatMessages(chat.id);

  // Transform DrizzleMessages to AIMessages
  const initialMessages: AIMessage[] = dbMessages.map((dbMsg): AIMessage => {
    return {
      id: dbMsg.id,
      role: dbMsg.role as AIMessage['role'], // Cast role, ensure AIMessage['role'] includes all dbMsg.role possibilities
      content: dbMsg.content,
      createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined,
      // 'type' and 'name' are not in the basic Drizzle 'messages' schema.
      // These would be undefined unless specific logic is added to derive them.
      // For instance, if a message with role 'tool' should have a 'name',
      // or if some messages have a specific 'type' based on content or other flags.
      // This mapping assumes standard user/assistant messages primarily.
    };
  });

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions