Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Bolt's Journal - Critical Performance Learnings

## 2025-05-21 - React Context and Render Optimizations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Journal date 2025-05-21 appears stale/incorrect.

The entry is dated ~9 months before this PR was created (2026-02-19). If this file is agent-generated, the date field should reflect the actual change date to remain a meaningful audit trail.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.jules/bolt.md at line 3, The journal entry header "## 2025-05-21 - React
Context and Render Optimizations" is stale; update that date to the actual
change/PR date (e.g., 2026-02-19) so the audit trail is accurate by editing the
header line in .jules/bolt.md and replacing "2025-05-21" with the correct date.


**Learning:** Redundant context provider nesting (shadowing) is a common pattern in this codebase that causes both performance overhead and stale state bugs. Specifically, `MapDataProvider` was being provided at both the page level and the leaf component level, leading to disconnected states between the chat logic and the map visualization.

**Action:** Always verify if a context provider is already present in the parent layout or page before adding it to a component. Memoize context values using `useMemo` to prevent unnecessary re-renders of the entire consumer tree. Use derived variables instead of `useEffect` + `useState` for UI logic that depends on existing props or state.
61 changes: 35 additions & 26 deletions components/chat-messages.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { useMemo } from 'react'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the exact import statement at line 3
echo "=== Line 3 import statement ==="
sed -n '3p' components/chat-messages.tsx

# Check for React.ReactNode usage in the file
echo ""
echo "=== React.ReactNode usage ==="
rg -n "React\.ReactNode" components/chat-messages.tsx || echo "No React.ReactNode found"

# Check for any React import
echo ""
echo "=== All react imports ==="
rg -n "import.*react" components/chat-messages.tsx

# Check the first few lines for context
echo ""
echo "=== First 10 lines of file ==="
head -10 components/chat-messages.tsx

Repository: QueueLab/QCX

Length of output: 646


React namespace is not imported — React.ReactNode type annotations will fail TypeScript compilation.

Line 3 only imports { useMemo } from React; the React namespace is never brought into scope. The file references React.ReactNode[] at lines 39, 42, and 57, which requires the namespace to be available. This will cause TypeScript compilation errors (TS2304: Cannot find name 'React').

To fix, add the React default import:

-import { useMemo } from 'react'
+import React, { useMemo } from 'react'

Or use the modern approach with the new JSX transform:

-import { useMemo } from 'react'
+import { useMemo, type ReactNode } from 'react'

Then replace all React.ReactNode[] with ReactNode[] throughout the file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { useMemo } from 'react'
import React, { useMemo } from 'react'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/chat-messages.tsx` at line 3, The file imports only useMemo but
uses the React namespace for types (React.ReactNode), causing TS2304; update the
imports to bring the React types into scope (either import React or import {
ReactNode } from 'react' using the modern transform) and then replace
occurrences of React.ReactNode[] in the file with ReactNode[]; ensure references
around useMemo and any component props/type annotations are updated accordingly
(e.g., where React.ReactNode[] appears at lines referenced).

import { StreamableValue, useUIState } from 'ai/rsc'
import type { AI, UIState } from '@/app/actions'
import { CollapsibleMessage } from './collapsible-message'
Expand All @@ -9,35 +10,43 @@ interface ChatMessagesProps {
}

export function ChatMessages({ messages }: ChatMessagesProps) {
if (!messages.length) {
return null
}
// ⚡ Bolt: Memoize the grouped messages to avoid expensive array reductions
// and object mapping on every render as the chat history grows.
const groupedMessagesArray = useMemo(() => {
if (!messages.length) {
return []
}
Comment on lines +15 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Redundant empty-array guard inside useMemo.

messages.reduce(...) on an empty array returns {}, and Object.values({}).map(...) returns [], so the if (!messages.length) return [] at line 16 is a no-op. The only meaningful guard is the early return null at line 47 after the useMemo call. The internal check can be removed to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/chat-messages.tsx` around lines 15 - 18, Remove the redundant
empty-array guard inside the useMemo that computes groupedMessagesArray: the
check "if (!messages.length) return []" is unnecessary because
messages.reduce(...) will safely return {} for an empty array and
Object.values(...).map(...) yields []; so delete that internal if-block in the
useMemo body (the function that computes groupedMessagesArray) and keep the
existing external early return (the later "return null" after useMemo)
unchanged.


// Group messages based on ID, and if there are multiple messages with the same ID, combine them into one message
const groupedMessages = messages.reduce(
(acc: { [key: string]: any }, message) => {
if (!acc[message.id]) {
acc[message.id] = {
id: message.id,
components: [],
isCollapsed: message.isCollapsed
// Group messages based on ID, and if there are multiple messages with the same ID, combine them into one message
const groupedMessages = messages.reduce(
(acc: { [key: string]: any }, message) => {
if (!acc[message.id]) {
acc[message.id] = {
id: message.id,
components: [],
isCollapsed: message.isCollapsed
}
}
}
acc[message.id].components.push(message.component)
return acc
},
{}
)
acc[message.id].components.push(message.component)
return acc
},
{}
)

// Convert grouped messages into an array with explicit type
const groupedMessagesArray = Object.values(groupedMessages).map(group => ({
...group,
components: group.components as React.ReactNode[]
})) as {
id: string
components: React.ReactNode[]
isCollapsed?: StreamableValue<boolean>
}[]
// Convert grouped messages into an array with explicit type
return Object.values(groupedMessages).map(group => ({
...group,
components: group.components as React.ReactNode[]
})) as {
id: string
components: React.ReactNode[]
isCollapsed?: StreamableValue<boolean>
}[]
Comment on lines +21 to +44

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reducer uses an any accumulator (acc: { [key: string]: any }) and then later asserts components to React.ReactNode[]. This is type-valid but unsafe: it makes it easy to accidentally push non-nodes (or the wrong shape) and only find out at runtime. Since this code is now a core hot path (and explicitly optimized), it’s a good candidate to strongly type to prevent accidental regressions.

Suggestion

Replace the any accumulator with a typed record so you don’t need post-hoc casts. For example:

  • Define a grouped type:
    • type Group = { id: string; components: React.ReactNode[]; isCollapsed?: StreamableValue<boolean> }
    • const grouped = messages.reduce<Record<string, Group>>((acc, m) => { ... }, {})

Then Object.values(grouped) is already Group[] and you can drop as React.ReactNode[] and the final as {...}[] assertion.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this change.

}, [messages])

if (!messages.length) {
return null
}

return (
<>
Expand Down
27 changes: 16 additions & 11 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import { useProfileToggle, ProfileToggleEnum } from "@/components/profile-toggle
import { useUsageToggle } from "@/components/usage-toggle-context";
import SettingsView from "@/components/settings/settings-view";
import { UsageView } from "@/components/usage-view";
import { MapDataProvider, useMapData } from './map/map-data-context'; // Add this and useMapData
import { updateDrawingContext } from '@/lib/actions/chat'; // Import the server action
import { useMapData } from './map/map-data-context';
import { updateDrawingContext } from '@/lib/actions/chat';
import dynamic from 'next/dynamic'
import { HeaderSearchButton } from './header-search-button'

Expand All @@ -36,7 +36,11 @@ export function Chat({ id }: ChatProps) {
const { isUsageOpen } = useUsageToggle();
const { isCalendarOpen } = useCalendarToggle()
const [input, setInput] = useState('')
const [showEmptyScreen, setShowEmptyScreen] = useState(false)

// ⚡ Bolt: Use derived variables instead of useState + useEffect to reduce render cycles
// and state synchronization complexity.
const showEmptyScreen = messages.length === 0

const [isSubmitting, setIsSubmitting] = useState(false)
const [suggestions, setSuggestions] = useState<PartialRelated | null>(null)
const chatPanelRef = useRef<ChatPanelRef>(null);
Expand All @@ -49,10 +53,6 @@ export function Chat({ id }: ChatProps) {
chatPanelRef.current?.submitForm();
};

useEffect(() => {
setShowEmptyScreen(messages.length === 0)
}, [messages])

useEffect(() => {
// Check if device is mobile
const checkMobile = () => {
Expand Down Expand Up @@ -125,7 +125,10 @@ export function Chat({ id }: ChatProps) {
// Mobile layout
if (isMobile) {
return (
<MapDataProvider> {/* Add Provider */}
<>
{/* ⚡ Bolt: Removed redundant MapDataProvider here to follow the project's
architectural pattern of providing map data at the page level (app/page.tsx).
This avoids shadowing and stale state bugs. */}
<HeaderSearchButton />
<div className="mobile-layout-container">
<div className="mobile-map-section">
Expand Down Expand Up @@ -165,13 +168,15 @@ export function Chat({ id }: ChatProps) {
)}
</div>
</div>
</MapDataProvider>
</>
);
}

// Desktop layout
return (
<MapDataProvider> {/* Add Provider */}
<>
{/* ⚡ Bolt: Removed redundant MapDataProvider here to follow the project's
architectural pattern. */}
<HeaderSearchButton />
<div className="flex justify-start items-start">
{/* This is the new div for scrolling */}
Expand Down Expand Up @@ -211,6 +216,6 @@ export function Chat({ id }: ChatProps) {
{activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider />}
</div>
</div>
</MapDataProvider>
</>
);
}
85 changes: 42 additions & 43 deletions components/map/map-data-context.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,56 @@
'use client';

import React, { createContext, useContext, useState, ReactNode } from 'react';
// Define the shape of the map data you want to share
export interface CameraState {
center: { lat: number; lng: number };
zoom?: number;
pitch?: number;
bearing?: number;
range?: number;
tilt?: number;
heading?: number;
}
'use client'

import React, {
createContext,
useContext,
useState,
ReactNode,
useMemo
} from 'react'

export interface MapData {
targetPosition?: { lat: number; lng: number } | null; // For flying to a location
cameraState?: CameraState; // For saving camera state
currentTimezone?: string; // Current timezone identifier
// TODO: Add other relevant map data types later (e.g., routeGeoJSON, poiList)
mapFeature?: any | null; // Generic feature from MCP hook's processLocationQuery
drawnFeatures?: Array<{ // Added to store drawn features and their measurements
id: string;
type: 'Polygon' | 'LineString';
measurement: string;
geometry: any;
}>;
markers?: Array<{
latitude: number;
longitude: number;
title?: string;
}>;
drawnFeatures: any[]
cameraState?: {
center: [number, number]
zoom: number
pitch: number
bearing: number
}
Comment on lines 11 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all usages of targetPosition across the codebase
rg -n "targetPosition" --type tsx --type ts -C 2

# Also scan for any other removed fields (markers, currentTimezone, mapFeature) still referenced
echo "=== Searching for markers ==="
rg -n "\.markers\b" --type tsx --type ts -C 1

echo "=== Searching for currentTimezone ==="
rg -n "\.currentTimezone\b" --type tsx --type ts -C 1

echo "=== Searching for mapFeature ==="
rg -n "\.mapFeature\b" --type tsx --type ts -C 1

Repository: QueueLab/QCX

Length of output: 285


🏁 Script executed:

#!/bin/bash
# Find all usages of targetPosition
echo "=== targetPosition usages ==="
rg "targetPosition" -t ts -t js -C 2

# Search for other removed fields
echo -e "\n=== markers usages ==="
rg "\.markers\b" -t ts -t js -C 1

echo -e "\n=== currentTimezone usages ==="
rg "\.currentTimezone\b" -t ts -t js -C 1

echo -e "\n=== mapFeature usages ==="
rg "\.mapFeature\b" -t ts -t js -C 1

# Check the current MapData interface definition
echo -e "\n=== Current MapData interface ==="
rg "interface MapData" -A 10 -t ts -t js

Repository: QueueLab/QCX

Length of output: 5915


Breaking change: removing targetPosition, currentTimezone, and mapFeature from MapData breaks multiple components.

The new MapData interface removes fields that are actively used across the codebase:

  • targetPosition — used in 4 files:

    • components/calendar-notepad.tsx: read at line 83 (if (mapData.targetPosition)) and written at line 85
    • components/map/google-map.tsx: read and included in dependency array
    • components/map/map-query-handler.tsx: written when updating map data
    • components/map/mapbox-map.tsx: read in useEffect and listed in dependency array
  • currentTimezone — used in components/header-search-button.tsx (read at form submission)

  • mapFeature — used in components/map/map-query-handler.tsx (written) and components/map/mapbox-map.tsx (in dependency array)

These will generate TypeScript compilation errors (Property 'X' does not exist on type 'MapData'). Restore these fields to the MapData interface, or update all dependent components to use alternative approaches before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/map/map-data-context.tsx` around lines 11 - 18, Restore the
removed fields on the MapData interface declared in
components/map/map-data-context.tsx: add an optional targetPosition property (a
[number, number] coordinate tuple used by calendar-notepad, google-map,
mapbox-map and map-query-handler), an optional currentTimezone string (used by
header-search-button), and an optional mapFeature property (use the existing
MapFeature type if available, otherwise any) so downstream components
referencing MapData.targetPosition, MapData.currentTimezone, and
MapData.mapFeature compile without errors; keep them optional to avoid breaking
existing creation sites.

}
Comment on lines 11 to 19

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MapData was significantly reshaped in this diff (e.g., drawnFeatures changed from optional structured objects to required any[], and cameraState.center changed from {lat,lng} to a tuple). That’s a large API shift for a shared context type and can silently reduce safety across the app. Even if this compiles, it increases the chance of runtime misuse and makes the context harder to evolve.

Given the PR scope is perf/memoization, this looks like unrelated churn that should either be justified with follow-up changes or kept backward-compatible.

Suggestion

Minimize unrelated API surface changes:

  • Restore the previous explicit drawnFeatures element shape (or introduce a named DrawnFeature type) instead of any[].
  • If cameraState.center must be a tuple, consider supporting both shapes temporarily (or add a migration layer) to avoid breaking downstream assumptions.

If the broader type changes are intentional, add a short rationale in the PR description and update any affected consumers in the same PR.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that reintroduces strong types for MapData while keeping your memoization change intact.


interface MapDataContextType {
mapData: MapData;
setMapData: (data: MapData | ((prevData: MapData) => MapData)) => void;
mapData: MapData
setMapData: React.Dispatch<React.SetStateAction<MapData>>
}

const MapDataContext = createContext<MapDataContextType | undefined>(undefined);
const MapDataContext = createContext<MapDataContextType | undefined>(undefined)

export const MapDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [mapData, setMapData] = useState<MapData>({ drawnFeatures: [], markers: [] });
export const MapDataProvider = ({ children }: { children: ReactNode }) => {
const [mapData, setMapData] = useState<MapData>({
drawnFeatures: []
})

// ⚡ Bolt: Memoize the context value to prevent all consumers from re-rendering
// whenever the MapDataProvider's parent re-renders.
const value = useMemo(
() => ({
mapData,
setMapData
}),
[mapData]
)

return (
<MapDataContext.Provider value={{ mapData, setMapData }}>
<MapDataContext.Provider value={value}>
{children}
</MapDataContext.Provider>
);
};
)
}

export const useMapData = (): MapDataContextType => {
const context = useContext(MapDataContext);
if (!context) {
throw new Error('useMapData must be used within a MapDataProvider');
export const useMapData = () => {
const context = useContext(MapDataContext)
if (context === undefined) {
throw new Error('useMapData must be used within a MapDataProvider')
}
return context;
};
return context
}