You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PDF Report Generation — Implementation Plan for QCX
This feature does not exist yet in the codebase. There are zero existing PDF, report, export, or download utilities anywhere in the project. However, all the raw data you need is already being captured and stored. Here is a precise breakdown of what exists and exactly what needs to be built.
What Data Is Already Available
1. Questions & Responses (Chat Messages)
The full conversation history lives in AIState.messages, an array of AIMessage objects. Each message has a type field that distinguishes user questions from AI answers:
User questions: type === 'input' or type === 'input_related'
AI answers: type === 'response'
Map analysis results: type === 'resolution_search_result'1
The AIState shape (holding all messages for the session) and the getUIStateFromAIState function (which reconstructs the full conversation from stored state) are both in app/actions.tsx: 23
Messages are persisted to a Postgres database via the messages and chats tables and can be retrieved by chat ID using getMessagesByChatId: 45
2. Map Screenshots (Images)
When a resolution search is run, the Mapbox screenshot, Google Satellite screenshot, and original image are all captured as base64 data URLs and stored directly in the resolution_search_result message content: 6
These are later parsed and rendered in ResolutionCarousel and ResolutionImage components: 7
Additionally, the live Mapbox map instance is initialized with preserveDrawingBuffer: true, which means at any moment you can call map.getCanvas().toDataURL() to capture the current map view as a PNG — even with drawn features visible: 8
The live map instance is accessible globally via the MapContext / useMap() hook: 9
3. Drawn Features & Measurements
All polygons and line strings drawn by the user, along with their computed area/distance measurements, are stored in MapData.drawnFeatures via the MapDataContext. Each entry contains the feature type, its geometry (GeoJSON), and its human-readable measurement string: 10
These are computed and stored whenever a shape is drawn or updated via updateMeasurementLabels, which builds the currentDrawnFeatures array from MapboxDraw's state: 11
The drawn features are also passed to the AI on every form submission via a hidden drawnFeatures field: 12
4. GeoJSON Layers
When the AI performs a geospatial analysis, it may return a geoJson field inside the resolution_search_result message, which is rendered as a map overlay: 13
What Needs to Be Built
Step 1 — Add a PDF Library
No PDF library exists in the project dependencies: 14
You need to add one. The two most compatible options with this Next.js 15 / React 19 stack are:
@react-pdf/renderer — compose the PDF as React components; works entirely client-side
jsPDF + html2canvas — screenshot DOM sections and embed them; simpler for rich content
Step 2 — Create a PDF Report Generator Utility
Create a new file (e.g., lib/utils/report-generator.ts) that accepts the following inputs:
messages: AIMessage[] — from getAIState() or useAIState() (already typed in lib/types/index.ts)
drawnFeatures: MapData['drawnFeatures'] — from useMapData() context
mapSnapshot: string — from map.getCanvas().toDataURL() using the useMap() hook
chatTitle: string — from the chats table title
The report structure should be:
1. Cover page (chat title, date, map snapshot)
2. For each Q&A turn:
a. User question (type: 'input' or 'input_related')
b. AI response (type: 'response') — render markdown
c. Map images if present (type: 'resolution_search_result' → image, mapboxImage, googleImage)
d. GeoJSON summary if present
3. Drawings appendix (each drawn feature: type, measurement, coordinates)
Step 3 — Add a "Download Report" Button
The best place to add this is in the header, next to the existing action buttons (Calendar, Map Toggle, etc.): 15
The button component should:
Call useAIState() to read aiState.messages
Call useMapData() to read mapData.drawnFeatures
Call useMap() to get the live map instance and call .getCanvas().toDataURL()
Call the report generator, then trigger a browser download via URL.createObjectURL(blob)
The sharing button in chat-share.tsx is fully disabled and returns null, so there is no existing share/export UI to reuse: 16
Step 4 — Wire Up Message Filtering
Use the same filtering logic already applied in submit() to isolate only user-facing content (exclude 'tool', 'followup', 'related', 'end' types): 17
For 'resolution_search_result' messages, parse the JSON content to extract image, mapboxImage, googleImage, and geoJson: 7
Architecture Diagram
flowchart TD
A["User clicks 'Download Report' button"] --> B["useAIState() → AIMessage[]"]
A --> C["useMapData() → drawnFeatures[]"]
A --> D["useMap() → map.getCanvas().toDataURL()"]
B --> E["Filter messages:\n input, response,\n resolution_search_result"]
E --> F["Parse Q&A pairs"]
E --> G["Parse map images\n (mapboxImage, googleImage, image)"]
E --> H["Parse GeoJSON layers"]
C --> I["Drawings appendix:\n type + measurement + geometry"]
D --> J["Live map snapshot PNG"]
F & G & H & I & J --> K["PDF Composer\n (@react-pdf/renderer or jsPDF)"]
K --> L["Blob download triggered\n in browser"]
Loading
Notes
preserveDrawingBuffer: true on the Mapbox map is the critical enabler for live map screenshots. This is already set in the codebase and means no workaround is needed.
The compressImage utility in lib/utils/image-utils.ts can be reused to reduce the size of map images before embedding them in the PDF. 18
Chat history is persisted to Postgres, so reports can also be generated server-side (e.g., in a new API route under app/api/) from a past chat ID using getMessagesByChatId(chatId), not just from the live session. 4
The calendarNotes table also links notes to chats and could optionally be included in the report as a "scheduled observations" section. 19
The visualizations table stores GeoJSON map layers linked to chats, which could serve as an additional data source for reports. 20
Citations
File: lib/types/index.ts (L59-77)
exporttypeAIMessage={role: 'user'|'assistant'|'system'|'function'|'data'|'tool'content: CoreMessage['content']id: stringname?: stringcreatedAt?: Date// Added optional createdAt timestamptype?:
|'response'|'related'|'skip'|'inquiry'|'input'|'input_related'|'tool'|'followup'|'end'|'drawing_context'// Added custom type for drawing context messages|'resolution_search_result'}
exportasyncfunctiongetMessagesByChatId(chatId: string): Promise<Message[]>{if(!chatId){console.warn('getMessagesByChatId called without chatId');return[];}try{constresult=awaitdb.select().from(messages).where(eq(messages.chatId,chatId)).orderBy(asc(messages.createdAt));// Order messages chronologicallyreturnresult;}catch(error){console.error(`Error fetching messages for chat ${chatId}:`,error);return[];}}
'use client'import{createContext,useContext,useState,ReactNode}from'react'importtype{MapasMapboxMap}from'mapbox-gl'// A more direct context to hold the map instance itself.typeMapContextType={map: MapboxMap|null;setMap: (map: MapboxMap|null)=>void;};constMapContext=createContext<MapContextType|undefined>(undefined);exportconstMapProvider=({ children }: {children: ReactNode})=>{const[map,setMap]=useState<MapboxMap|null>(null);return(<MapContext.Providervalue={{map,setMap}}>{children}</MapContext.Provider>);};exportconstuseMap=(): MapContextType=>{constcontext=useContext(MapContext);if(!context){thrownewError('useMap must be used within a MapProvider');}returncontext;};
exportinterfaceMapData{targetPosition?: {lat: number;lng: number}|null;// For flying to a locationcameraState?: CameraState;// For saving camera statecurrentTimezone?: 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 processLocationQuerydrawnFeatures?: Array<{// Added to store drawn features and their measurementsid: string;type: 'Polygon'|'LineString';measurement: string;geometry: any;}>;markers?: Array<{latitude: number;longitude: number;title?: string;}>;}
File: components/chat-panel.tsx (L118-120)
// Include drawn features in the form dataformData.append('drawnFeatures',JSON.stringify(mapData.drawnFeatures||[]))
// TODO: Re-evaluate/reimplement sharing with Supabase. For now, disable the UI.if(true){// Conditionally disable the share button/dialogreturnnull;// Or return a disabled button: <Button className={className} variant="ghost" size="icon" disabled><Share size={14} /></Button>}
File: lib/utils/image-utils.ts (L4-51)
exportasyncfunctioncompressImage(blob: Blob,maxWidth=1024,maxHeight=1024,quality=0.8): Promise<Blob>{returnnewPromise((resolve,reject)=>{constimg=newImage();img.onload=()=>{letwidth=img.width;letheight=img.height;if(width>height){if(width>maxWidth){height*=maxWidth/width;width=maxWidth;}}else{if(height>maxHeight){width*=maxHeight/height;height=maxHeight;}}constcanvas=document.createElement('canvas');canvas.width=width;canvas.height=height;constctx=canvas.getContext('2d');if(!ctx){reject(newError('Failed to get canvas context'));return;}ctx.drawImage(img,0,0,width,height);canvas.toBlob((compressedBlob)=>{if(compressedBlob){resolve(compressedBlob);}else{reject(newError('Failed to compress image'));}},'image/jpeg',quality);};img.onerror=()=>reject(newError('Failed to load image for compression'));img.src=URL.createObjectURL(blob);});}
PDF Report Generation — Implementation Plan for QCX
This feature does not exist yet in the codebase. There are zero existing PDF, report, export, or download utilities anywhere in the project. However, all the raw data you need is already being captured and stored. Here is a precise breakdown of what exists and exactly what needs to be built.
What Data Is Already Available
1. Questions & Responses (Chat Messages)
The full conversation history lives in
AIState.messages, an array ofAIMessageobjects. Each message has atypefield that distinguishes user questions from AI answers:type === 'input'ortype === 'input_related'type === 'response'type === 'resolution_search_result'1The
AIStateshape (holding all messages for the session) and thegetUIStateFromAIStatefunction (which reconstructs the full conversation from stored state) are both inapp/actions.tsx: 2 3Messages are persisted to a Postgres database via the
messagesandchatstables and can be retrieved by chat ID usinggetMessagesByChatId: 4 52. Map Screenshots (Images)
When a resolution search is run, the Mapbox screenshot, Google Satellite screenshot, and original image are all captured as base64 data URLs and stored directly in the
resolution_search_resultmessage content: 6These are later parsed and rendered in
ResolutionCarouselandResolutionImagecomponents: 7Additionally, the live Mapbox map instance is initialized with
preserveDrawingBuffer: true, which means at any moment you can callmap.getCanvas().toDataURL()to capture the current map view as a PNG — even with drawn features visible: 8The live map instance is accessible globally via the
MapContext/useMap()hook: 93. Drawn Features & Measurements
All polygons and line strings drawn by the user, along with their computed area/distance measurements, are stored in
MapData.drawnFeaturesvia theMapDataContext. Each entry contains the featuretype, itsgeometry(GeoJSON), and its human-readablemeasurementstring: 10These are computed and stored whenever a shape is drawn or updated via
updateMeasurementLabels, which builds thecurrentDrawnFeaturesarray from MapboxDraw's state: 11The drawn features are also passed to the AI on every form submission via a hidden
drawnFeaturesfield: 124. GeoJSON Layers
When the AI performs a geospatial analysis, it may return a
geoJsonfield inside theresolution_search_resultmessage, which is rendered as a map overlay: 13What Needs to Be Built
Step 1 — Add a PDF Library
No PDF library exists in the project dependencies: 14
You need to add one. The two most compatible options with this Next.js 15 / React 19 stack are:
@react-pdf/renderer— compose the PDF as React components; works entirely client-sidejsPDF+html2canvas— screenshot DOM sections and embed them; simpler for rich contentStep 2 — Create a PDF Report Generator Utility
Create a new file (e.g.,
lib/utils/report-generator.ts) that accepts the following inputs:messages: AIMessage[]— fromgetAIState()oruseAIState()(already typed inlib/types/index.ts)drawnFeatures: MapData['drawnFeatures']— fromuseMapData()contextmapSnapshot: string— frommap.getCanvas().toDataURL()using theuseMap()hookchatTitle: string— from thechatstable titleThe report structure should be:
Step 3 — Add a "Download Report" Button
The best place to add this is in the header, next to the existing action buttons (Calendar, Map Toggle, etc.): 15
The button component should:
useAIState()to readaiState.messagesuseMapData()to readmapData.drawnFeaturesuseMap()to get the live map instance and call.getCanvas().toDataURL()URL.createObjectURL(blob)The sharing button in
chat-share.tsxis fully disabled and returnsnull, so there is no existing share/export UI to reuse: 16Step 4 — Wire Up Message Filtering
Use the same filtering logic already applied in
submit()to isolate only user-facing content (exclude'tool','followup','related','end'types): 17For
'resolution_search_result'messages, parse the JSON content to extractimage,mapboxImage,googleImage, andgeoJson: 7Architecture Diagram
flowchart TD A["User clicks 'Download Report' button"] --> B["useAIState() → AIMessage[]"] A --> C["useMapData() → drawnFeatures[]"] A --> D["useMap() → map.getCanvas().toDataURL()"] B --> E["Filter messages:\n input, response,\n resolution_search_result"] E --> F["Parse Q&A pairs"] E --> G["Parse map images\n (mapboxImage, googleImage, image)"] E --> H["Parse GeoJSON layers"] C --> I["Drawings appendix:\n type + measurement + geometry"] D --> J["Live map snapshot PNG"] F & G & H & I & J --> K["PDF Composer\n (@react-pdf/renderer or jsPDF)"] K --> L["Blob download triggered\n in browser"]Notes
preserveDrawingBuffer: trueon the Mapbox map is the critical enabler for live map screenshots. This is already set in the codebase and means no workaround is needed.compressImageutility inlib/utils/image-utils.tscan be reused to reduce the size of map images before embedding them in the PDF. 18app/api/) from a past chat ID usinggetMessagesByChatId(chatId), not just from the live session. 4calendarNotestable also links notes to chats and could optionally be included in the report as a "scheduled observations" section. 19visualizationstable stores GeoJSON map layers linked to chats, which could serve as an additional data source for reports. 20Citations
File: lib/types/index.ts (L59-77)
File: app/actions.tsx (L116-124)
File: app/actions.tsx (L159-183)
File: app/actions.tsx (L311-328)
File: app/actions.tsx (L561-572)
File: app/actions.tsx (L662-848)
File: lib/actions/chat-db.ts (L198-214)
File: lib/db/schema.ts (L52-61)
File: lib/db/schema.ts (L82-90)
File: lib/db/schema.ts (L92-103)
File: components/map/mapbox-map.tsx (L67-168)
File: components/map/mapbox-map.tsx (L396-406)
File: components/map/map-context.tsx (L1-30)
File: components/map/map-data-context.tsx (L15-32)
File: components/chat-panel.tsx (L118-120)
File: package.json (L18-101)
File: components/header.tsx (L69-87)
File: components/chat-share.tsx (L63-66)
File: lib/utils/image-utils.ts (L4-51)