-
-
Notifications
You must be signed in to change notification settings - Fork 7
GeoJSON Upload and Tool Ingestion Pipeline #472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
1b56819
9f79929
bfe62f9
9450b15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -23,6 +23,7 @@ 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 { MapDataUpdater } from '@/components/map/map-data-updater' | ||||||||||||||||||||||||||||||||||||||||
| import { CopilotDisplay } from '@/components/copilot-display' | ||||||||||||||||||||||||||||||||||||||||
| import RetrieveSection from '@/components/retrieve-section' | ||||||||||||||||||||||||||||||||||||||||
| import { VideoSearchSection } from '@/components/video-search-section' | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -210,6 +211,37 @@ async function submit(formData?: FormData, skip?: boolean) { | |||||||||||||||||||||||||||||||||||||||
| : ((formData?.get('related_query') as string) || | ||||||||||||||||||||||||||||||||||||||||
| (formData?.get('input') as string)) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| let isGeoJsonInput = false | ||||||||||||||||||||||||||||||||||||||||
| if (userInput) { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| const trimmedInput = userInput.trim() | ||||||||||||||||||||||||||||||||||||||||
| if ((trimmedInput.startsWith('{') && trimmedInput.endsWith('}')) || (trimmedInput.startsWith('[') && trimmedInput.endsWith(']'))) { | ||||||||||||||||||||||||||||||||||||||||
| const geoJson = JSON.parse(trimmedInput) | ||||||||||||||||||||||||||||||||||||||||
| if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') { | ||||||||||||||||||||||||||||||||||||||||
| isGeoJsonInput = true | ||||||||||||||||||||||||||||||||||||||||
| const geoJsonId = nanoid() | ||||||||||||||||||||||||||||||||||||||||
| aiState.update({ | ||||||||||||||||||||||||||||||||||||||||
| ...aiState.get(), | ||||||||||||||||||||||||||||||||||||||||
| messages: [ | ||||||||||||||||||||||||||||||||||||||||
| ...aiState.get().messages, | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| id: geoJsonId, | ||||||||||||||||||||||||||||||||||||||||
| role: 'assistant', | ||||||||||||||||||||||||||||||||||||||||
| content: JSON.stringify({ data: geoJson, filename: 'Pasted GeoJSON' }), | ||||||||||||||||||||||||||||||||||||||||
| type: 'geojson_upload' | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| uiStream.append( | ||||||||||||||||||||||||||||||||||||||||
| <MapDataUpdater id={geoJsonId} data={geoJson} filename="Pasted GeoJSON" /> | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||
| // Not a valid JSON, ignore | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (userInput.toLowerCase().trim() === 'what is a planet computer?' || userInput.toLowerCase().trim() === 'what is qcx-terra?') { | ||||||||||||||||||||||||||||||||||||||||
| const definition = userInput.toLowerCase().trim() === 'what is a planet computer?' | ||||||||||||||||||||||||||||||||||||||||
| ? `A planet computer is a proprietary environment aware system that interoperates weather forecasting, mapping and scheduling using cutting edge multi-agents to streamline automation and exploration on a planet. Available for our Pro and Enterprise customers. [QCX Pricing](https://www.queue.cx/#pricing)` | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -301,6 +333,8 @@ async function submit(formData?: FormData, skip?: boolean) { | |||||||||||||||||||||||||||||||||||||||
| }[] = [] | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (userInput) { | ||||||||||||||||||||||||||||||||||||||||
| // If it's a GeoJSON input, we still want to keep it in the message history for the AI to see, | ||||||||||||||||||||||||||||||||||||||||
| // but we might want to truncate it if it's huge. For now, just pass it. | ||||||||||||||||||||||||||||||||||||||||
| messageParts.push({ type: 'text', text: userInput }) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -315,8 +349,39 @@ async function submit(formData?: FormData, skip?: boolean) { | |||||||||||||||||||||||||||||||||||||||
| image: dataUrl, | ||||||||||||||||||||||||||||||||||||||||
| mimeType: file.type | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| } else if (file.type === 'text/plain') { | ||||||||||||||||||||||||||||||||||||||||
| } else if (file.type === 'text/plain' || file.name.endsWith('.geojson') || file.type === 'application/geo+json') { | ||||||||||||||||||||||||||||||||||||||||
| const textContent = Buffer.from(buffer).toString('utf-8') | ||||||||||||||||||||||||||||||||||||||||
| const isGeoJson = file.name.endsWith('.geojson') || file.type === 'application/geo+json' | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (isGeoJson) { | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| const geoJson = JSON.parse(textContent) | ||||||||||||||||||||||||||||||||||||||||
| if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') { | ||||||||||||||||||||||||||||||||||||||||
| const geoJsonId = nanoid() | ||||||||||||||||||||||||||||||||||||||||
| // Add a special message to track the GeoJSON upload | ||||||||||||||||||||||||||||||||||||||||
| aiState.update({ | ||||||||||||||||||||||||||||||||||||||||
| ...aiState.get(), | ||||||||||||||||||||||||||||||||||||||||
| messages: [ | ||||||||||||||||||||||||||||||||||||||||
| ...aiState.get().messages, | ||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||
| id: geoJsonId, | ||||||||||||||||||||||||||||||||||||||||
| role: 'assistant', | ||||||||||||||||||||||||||||||||||||||||
| content: JSON.stringify({ data: geoJson, filename: file.name }), | ||||||||||||||||||||||||||||||||||||||||
| type: 'geojson_upload' | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Immediately append the updater to the UI stream | ||||||||||||||||||||||||||||||||||||||||
| uiStream.append( | ||||||||||||||||||||||||||||||||||||||||
| <MapDataUpdater id={geoJsonId} data={geoJson} filename={file.name} /> | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+360
to
+379
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Server action appends a React client component into the UI stream
This is fragile for a few reasons:
Given you already insert a SuggestionRemove the immediate If you need instant feedback before the assistant message is produced, consider appending a lightweight server-renderable placeholder (e.g., “Uploaded X”) and let the client handle map updates based on AI state. Reply with "@CharlieHelps yes please" if you'd like me to add a commit making this change. |
||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to parse GeoJSON:', e) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+352
to
+383
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent error handling may confuse users when GeoJSON parsing fails. When 🛡️ Proposed improvement } catch (e) {
console.error('Failed to parse GeoJSON:', e)
+ // Optionally notify the user
+ uiStream.append(
+ <BotMessage content={createStreamableValue(`⚠️ Failed to parse ${file.name} as valid GeoJSON. The file will be processed as plain text.`).value} />
+ )
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const existingTextPart = messageParts.find(p => p.type === 'text') | ||||||||||||||||||||||||||||||||||||||||
| if (existingTextPart) { | ||||||||||||||||||||||||||||||||||||||||
| existingTextPart.text = `${textContent}\n\n${existingTextPart.text}` | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -624,10 +689,19 @@ export const AI = createAI<AIState, UIState>({ | |||||||||||||||||||||||||||||||||||||||
| export const getUIStateFromAIState = (aiState: AIState): UIState => { | ||||||||||||||||||||||||||||||||||||||||
| const chatId = aiState.chatId | ||||||||||||||||||||||||||||||||||||||||
| const isSharePage = aiState.isSharePage | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Filter messages to only include the last 'data' message if multiple exist | ||||||||||||||||||||||||||||||||||||||||
| const lastDataMessageIndex = [...aiState.messages].reverse().findIndex(m => m.role === 'data') | ||||||||||||||||||||||||||||||||||||||||
| const actualLastDataIndex = lastDataMessageIndex === -1 ? -1 : aiState.messages.length - 1 - lastDataMessageIndex | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return aiState.messages | ||||||||||||||||||||||||||||||||||||||||
| .map((message, index) => { | ||||||||||||||||||||||||||||||||||||||||
| const { role, content, id, type, name } = message | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (role === 'data' && index !== actualLastDataIndex) { | ||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||
| !type || | ||||||||||||||||||||||||||||||||||||||||
| type === 'end' || | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -716,6 +790,13 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { | |||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| case 'geojson_upload': { | ||||||||||||||||||||||||||||||||||||||||
| const { data, filename } = JSON.parse(content as string) | ||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||||||||
| component: <MapDataUpdater id={id} data={data} filename={filename} /> | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+793
to
+799
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unvalidated
|
||||||||||||||||||||||||||||||||||||||||
| case 'geojson_upload': { | |
| const { data, filename } = JSON.parse(content as string) | |
| return { | |
| id, | |
| component: <MapDataUpdater id={id} data={data} filename={filename} /> | |
| } | |
| } | |
| case 'geojson_upload': { | |
| try { | |
| const { data, filename } = JSON.parse(content as string) | |
| return { | |
| id, | |
| component: <MapDataUpdater id={id} data={data} filename={filename} /> | |
| } | |
| } catch (e) { | |
| console.error('Error parsing geojson_upload content:', e) | |
| return { id, component: null } | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 780 - 786, The JSON.parse in the
'geojson_upload' case can throw for malformed content; wrap the parse of content
in a try-catch (the case handling that returns component: <MapDataUpdater ...
/>) similar to the other role branches: on success return the existing object
with id and component: <MapDataUpdater id={id} data={data} filename={filename}
/>, on parse failure catch the error, log or report it, and return a safe
fallback entry (e.g., an object with id and a lightweight error/display
component or null component) so UI state reconstruction doesn't crash; reference
the 'geojson_upload' case, JSON.parse(content as string), and MapDataUpdater
when applying the fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
data message rendering uses any and replays side effects without guarding
In the data case you iterate uploadedGeoJson.map((item: any) => <MapDataUpdater ... />). Two concerns:
- Using
anyhere makes it easy to introduce unsafe shapes that still compile. This is UI-state hydration code; it should be strict. - Rendering many
MapDataUpdaters will attemptfitBoundsfor each layer; on chat reload this can cause multiple sequential camera jumps. The currentMapDataUpdateronly de-dupes byidfor inserting into context, but it does not de-dupe thefitBoundsside effect. So reload may result in the map fitting to the last item every time, even if the user previously set a different view.
Suggestion
-
Replace
anywith a minimal structural type (e.g.,{ id: string; data: unknown; filename: string }) and validate required fields before rendering. -
Add a flag to
MapDataUpdaterlikeshouldFlyTo?: booleanand set it tofalsewhen replaying persisteduploadedGeoJsonfrom adatamessage, or add internal logic to onlyfitBoundson first-ever insert (e.g., only when it was newly added to context).
Reply with "@CharlieHelps yes please" if you'd like me to add a commit implementing shouldFlyTo and tightening the hydration type.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -48,14 +48,11 @@ export default async function SearchPage({ params }: SearchPageProps) { | |||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||
| role: dbMsg.role as AIMessage['role'], | ||||||||||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||||||||||
| type: dbMsg.type as AIMessage['type'], | ||||||||||||||||||||||||||||||||
| name: dbMsg.toolName as string, | ||||||||||||||||||||||||||||||||
|
Comment on lines
+51
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cd app/search && find . -type f -name "page.tsx" | head -5Repository: QueueLab/QCX Length of output: 71 🏁 Script executed: cd app && find . -type f -name "*.tsx" -o -name "*.ts" | grep -E "(page|types|message)" | head -20Repository: QueueLab/QCX Length of output: 89 🏁 Script executed: cat -n app/search/\[id\]/page.tsx | sed -n '40,65p'Repository: QueueLab/QCX Length of output: 1170 🏁 Script executed: # Find AIMessage type definition
rg -n "type AIMessage|interface AIMessage" --type=ts --type=tsxRepository: QueueLab/QCX Length of output: 83 🏁 Script executed: # Check dbMsg source and context
rg -n "dbMsg" app/search/\[id\]/page.tsx -B 5 -A 2Repository: QueueLab/QCX Length of output: 607 🏁 Script executed: rg -n "type AIMessage|interface AIMessage" -A 10Repository: QueueLab/QCX Length of output: 1274 🏁 Script executed: rg -n "type DrizzleMessage|interface DrizzleMessage" -A 10Repository: QueueLab/QCX Length of output: 38 🏁 Script executed: # Search for where these types are defined
fd -e ts -e tsx | xargs rg "export (type|interface) AIMessage"Repository: QueueLab/QCX Length of output: 100 🏁 Script executed: fd -e ts -e tsx | xargs rg "export (type|interface) DrizzleMessage"Repository: QueueLab/QCX Length of output: 40 🏁 Script executed: # Check for downstream usages of message.name and message.type
rg "\.name\s*\?" -t=ts -t=tsx | head -20Repository: QueueLab/QCX Length of output: 83 🏁 Script executed: # Look for optional chaining or null checks on message fields
rg "message\.(name|type|toolName)" | head -30Repository: QueueLab/QCX Length of output: 501 🏁 Script executed: # Find DrizzleMessage or Message type from Drizzle schema
rg -n "export type Message|export type DrizzleMessage" -A 15Repository: QueueLab/QCX Length of output: 1189 🏁 Script executed: # Check the database schema or Drizzle definitions
fd -e ts -e tsx | xargs rg "messages.*table\|export const messages"Repository: QueueLab/QCX Length of output: 40 🏁 Script executed: # Look at the Drizzle schema file
find . -name "*schema*" -o -name "*drizzle*" | head -10Repository: QueueLab/QCX Length of output: 549 🏁 Script executed: # Check db directory structure
ls -la db/ 2>/dev/null || ls -la lib/db/ 2>/dev/null || find . -type d -name "db" | head -3Repository: QueueLab/QCX Length of output: 349 🏁 Script executed: cat -n lib/db/schema.tsRepository: QueueLab/QCX Length of output: 4470 🏁 Script executed: # Check the actual schema definition for the messages table
cat -n lib/schema/messages.ts 2>/dev/null || cat -n lib/db/schema.ts | grep -A 30 "messages"Repository: QueueLab/QCX Length of output: 3827 🏁 Script executed: # Check the AIMessage type definition more completely
cat -n lib/types/index.ts | sed -n '59,75p'Repository: QueueLab/QCX Length of output: 614 🏁 Script executed: # Check if there are any null checks on message.type or message.name in the codebase
rg "message\.(type|name)\s*\?" -A 2 -B 2Repository: QueueLab/QCX Length of output: 38 🏁 Script executed: # Check how the initialMessages are used downstream
rg "initialMessages" app/search/\[id\]/page.tsx -A 5 -B 5Repository: QueueLab/QCX Length of output: 836 🏁 Script executed: # Check how the type and name fields are accessed in downstream code
rg "\.type\s*==|\.type\s*!=|\.type\s*\?|\.name\s*==|\.name\s*!=" app/ -A 2 -B 2Repository: QueueLab/QCX Length of output: 3401 🏁 Script executed: # Look at the AI component to see how it uses these fields
rg -n "initialAIState" app/ -A 10 | head -50Repository: QueueLab/QCX Length of output: 1910 🏁 Script executed: # Check if there are assertions or strict equality checks on type/name
rg "(type|name)\s*!==" app/ -B 2 -A 2Repository: QueueLab/QCX Length of output: 981 Convert nullable database fields to Lines 54–55 cast Recommended fix- type: dbMsg.type as AIMessage['type'],
- name: dbMsg.toolName as string,
+ type: dbMsg.type ?? undefined,
+ name: dbMsg.toolName ?? undefined,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -81,6 +81,9 @@ export function GoogleMapComponent() { | |||||||||||||||||
| mode="SATELLITE" | ||||||||||||||||||
| /> | ||||||||||||||||||
| <GoogleGeoJsonLayer data={featureCollection} /> | ||||||||||||||||||
| {mapData.uploadedGeoJson?.map(item => ( | ||||||||||||||||||
| item.visible && <GoogleGeoJsonLayer key={item.id} data={item.data} /> | ||||||||||||||||||
| ))} | ||||||||||||||||||
|
Comment on lines
+84
to
+86
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Conditional rendering inside When ♻️ Suggested refactor- {mapData.uploadedGeoJson?.map(item => (
- item.visible && <GoogleGeoJsonLayer key={item.id} data={item.data} />
- ))}
+ {mapData.uploadedGeoJson
+ ?.filter(item => item.visible)
+ .map(item => (
+ <GoogleGeoJsonLayer key={item.id} data={item.data} />
+ ))}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| </APIProvider> | ||||||||||||||||||
| ) | ||||||||||||||||||
| } | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -29,6 +29,12 @@ export interface MapData { | |||||||||||||||||||||||||
| longitude: number; | ||||||||||||||||||||||||||
| title?: string; | ||||||||||||||||||||||||||
| }>; | ||||||||||||||||||||||||||
| uploadedGeoJson?: Array<{ | ||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||
| filename: string; | ||||||||||||||||||||||||||
| data: any; // FeatureCollection | ||||||||||||||||||||||||||
| visible: boolean; | ||||||||||||||||||||||||||
| }>; | ||||||||||||||||||||||||||
|
Comment on lines
+32
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider typing The ♻️ Suggested type improvement+import type { FeatureCollection } from 'geojson';
+
// ... in MapData interface
uploadedGeoJson?: Array<{
id: string;
filename: string;
- data: any; // FeatureCollection
+ data: FeatureCollection;
visible: boolean;
}>;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Comment on lines
+32
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Even a lightweight validation (checking SuggestionReplace Example types: import type { Feature, FeatureCollection } from 'geojson'
data: FeatureCollection | FeatureReply with "@CharlieHelps yes please" if you’d like me to add a commit that tightens types and adds a shared |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| interface MapDataContextType { | ||||||||||||||||||||||||||
|
|
@@ -39,7 +45,11 @@ interface MapDataContextType { | |||||||||||||||||||||||||
| const MapDataContext = createContext<MapDataContextType | undefined>(undefined); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const MapDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => { | ||||||||||||||||||||||||||
| const [mapData, setMapData] = useState<MapData>({ drawnFeatures: [], markers: [] }); | ||||||||||||||||||||||||||
| const [mapData, setMapData] = useState<MapData>({ | ||||||||||||||||||||||||||
| drawnFeatures: [], | ||||||||||||||||||||||||||
| markers: [], | ||||||||||||||||||||||||||
| uploadedGeoJson: [] | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <MapDataContext.Provider value={{ mapData, setMapData }}> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import { useEffect, useRef } from 'react'; | ||||||||||||||||||||||||||||||
| import { useMapData } from './map-data-context'; | ||||||||||||||||||||||||||||||
| import { useMap } from './map-context'; | ||||||||||||||||||||||||||||||
| import type { FeatureCollection } from 'geojson'; | ||||||||||||||||||||||||||||||
| import * as turf from '@turf/turf'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| interface MapDataUpdaterProps { | ||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||
| data: any; // FeatureCollection or Feature | ||||||||||||||||||||||||||||||
| filename: string; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider stronger typing for the Using +import type { Feature } from 'geojson';
+
interface MapDataUpdaterProps {
id: string;
- data: any; // FeatureCollection or Feature
+ data: FeatureCollection | Feature | null;
filename: string;
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export function MapDataUpdater({ id, data, filename }: MapDataUpdaterProps) { | ||||||||||||||||||||||||||||||
| const { setMapData } = useMapData(); | ||||||||||||||||||||||||||||||
| const { map } = useMap(); | ||||||||||||||||||||||||||||||
| const hasZoomedRef = useRef(false); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||
| if (!data) return; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Ensure it's a FeatureCollection for consistency | ||||||||||||||||||||||||||||||
| const featureCollection: FeatureCollection = data.type === 'FeatureCollection' | ||||||||||||||||||||||||||||||
| ? data | ||||||||||||||||||||||||||||||
| : { type: 'FeatureCollection', features: [data] }; | ||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential silent failure with invalid GeoJSON data. The normalization logic assumes Consider adding validation: 🛡️ Proposed validation+ // Validate that data is a Feature or FeatureCollection
+ if (data.type !== 'FeatureCollection' && data.type !== 'Feature') {
+ console.warn('MapDataUpdater: Invalid GeoJSON type, expected Feature or FeatureCollection:', data.type);
+ return;
+ }
+
// Ensure it's a FeatureCollection for consistency
const featureCollection: FeatureCollection = data.type === 'FeatureCollection'
? data
: { type: 'FeatureCollection', features: [data] };📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Update MapData context | ||||||||||||||||||||||||||||||
| setMapData(prev => { | ||||||||||||||||||||||||||||||
| // Avoid duplicate entries | ||||||||||||||||||||||||||||||
| const alreadyExists = prev.uploadedGeoJson?.some(item => item.id === id); | ||||||||||||||||||||||||||||||
| if (alreadyExists) return prev; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||
| uploadedGeoJson: [ | ||||||||||||||||||||||||||||||
| ...(prev.uploadedGeoJson || []), | ||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||
| filename, | ||||||||||||||||||||||||||||||
| data: featureCollection, | ||||||||||||||||||||||||||||||
| visible: true | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Fly to the extent of the GeoJSON | ||||||||||||||||||||||||||||||
| if (map && featureCollection.features.length > 0 && !hasZoomedRef.current) { | ||||||||||||||||||||||||||||||
| hasZoomedRef.current = true; | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const bbox = turf.bbox(featureCollection); | ||||||||||||||||||||||||||||||
| map.fitBounds(bbox as [number, number, number, number], { | ||||||||||||||||||||||||||||||
| padding: 50, | ||||||||||||||||||||||||||||||
| maxZoom: 15, | ||||||||||||||||||||||||||||||
| duration: 2000 | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||
| console.error('Failed to fit bounds for GeoJSON:', e); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }, [id, data, filename, setMapData, map]); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return null; // Headless component | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider validating GeoJSON structure more thoroughly.
The current validation only checks
geoJson.type. Invalid GeoJSON (e.g., a FeatureCollection with malformed features) could still pass through and cause rendering issues downstream. Consider using a lightweight validation:🛡️ Optional: Add feature validation
if (geoJson.type === 'FeatureCollection' || geoJson.type === 'Feature') { + // Basic validation for FeatureCollection + if (geoJson.type === 'FeatureCollection' && !Array.isArray(geoJson.features)) { + console.warn('Invalid FeatureCollection: missing features array') + return + } const geoJsonId = nanoid()📝 Committable suggestion
🤖 Prompt for AI Agents