-
-
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
6eeae28
160e082
ae085cf
73c385c
1508c04
c8c028b
e2c0615
43179e7
7d45e02
5623831
44e86b6
0a48018
6acfbe5
8ae549a
3495da1
ba36e58
6431b41
8a04d49
10ab3fe
250283e
1b56819
9f79929
6554775
bfe62f9
9450b15
e95a25b
f80c87f
c4278e9
813d264
c868dcd
3b5ed27
3984b9b
f45f687
67c26d5
a842df1
184f678
895bf37
dc345b9
86013ed
2ba1f9e
dda7a32
23a1d3f
885dbbe
dd812c1
00c2e1a
54d9d6e
c9bd9b2
85a85c5
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' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+359
to
+373
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 validating GeoJSON structure more thoroughly. The current validation only checks 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
697
to
+703
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. Map callback does not explicitly return on all paths. The static analysis tool correctly identifies that the callback passed to The current code relies on the filter at line 890 to remove 🛡️ Proposed fix - add explicit return at switch default default:
return {
id,
component: null
}
}
+ return null // Explicit return for unhandled cases within switch
})
.filter(message => message !== null) as UIState📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.3.13)[error] 698-698: This callback passed to map() iterable method should always return a value. Add missing return statements so that this callback returns a value on all execution paths. (lint/suspicious/useIterableCallbackReturn) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !type || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type === 'end' || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -669,8 +743,8 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'assistant': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const answer = createStreamableValue() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| answer.done(content) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const answer = createStreamableValue(content as string) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| answer.done(content as string) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'response': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -682,7 +756,9 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case 'related': | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const relatedQueries = createStreamableValue<RelatedQueries>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const relatedQueries = createStreamableValue<RelatedQueries>({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items: [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| relatedQueries.done(JSON.parse(content as string)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -716,12 +792,19 @@ 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
+795
to
+801
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.
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.
Pasted GeoJSON is processed twice — once as upload and again as text input.
After successfully detecting and handling GeoJSON (lines 219-236), the code continues execution without returning or clearing
userInput. The raw JSON text is still added tomessagePartsat line 334 and sent to the AI model, wasting tokens and potentially confusing the model with raw coordinate data.Consider skipping the text message when GeoJSON is successfully parsed:
🐛 Proposed fix to prevent double processing
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') { 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" /> ) + // Skip normal text processing for GeoJSON input + isGenerating.done(false) + uiStream.done() + aiState.done(aiState.get()) + return { + id: nanoid(), + isGenerating: isGenerating.value, + component: uiStream.value, + isCollapsed: isCollapsed.value + } } } } catch (e) { // Not a valid JSON, ignore } }🤖 Prompt for AI Agents