Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
6eeae28
Integrate billing popup, usage sidebar, and credit preview toggle
CJWTRUST Jan 20, 2026
160e082
Recover lost commit 488b47c and restore branch head
google-labs-jules[bot] Jan 28, 2026
ae085cf
Merge pull request #456 from QueueLab/jules-16847885310673003402-db9c…
ngoiyaeric Jan 28, 2026
73c385c
Restore branch history to 488b47c and integrate latest fixes
google-labs-jules[bot] Jan 28, 2026
1508c04
Restore branch history to 488b47c and resolve build errors
google-labs-jules[bot] Jan 28, 2026
c8c028b
Restore branch history, fix build errors, and re-enable pricing popup
google-labs-jules[bot] Jan 28, 2026
e2c0615
Finalize recovery with performance optimizations and UI fixes
google-labs-jules[bot] Jan 28, 2026
43179e7
Synchronize with main, restore history, and optimize performance
google-labs-jules[bot] Jan 29, 2026
7d45e02
Finalize recovery with prioritized main branch code and usage UI
google-labs-jules[bot] Jan 29, 2026
5623831
Refine usage UI and Lottie animation visibility
google-labs-jules[bot] Jan 29, 2026
44e86b6
Complete recovery, synchronization with main, and usage UI integration
google-labs-jules[bot] Jan 29, 2026
0a48018
feat: recover branch state, optimize resolution search, and fix UI st…
google-labs-jules[bot] Jan 29, 2026
6acfbe5
fix: resolve build error and ESLint warnings
google-labs-jules[bot] Jan 29, 2026
8ae549a
Update LICENSE
ngoiyaeric Feb 1, 2026
3495da1
Update LICENSE
ngoiyaeric Feb 1, 2026
ba36e58
Merge pull request #469 from QueueLab/ngoiyaeric-patch-4
ngoiyaeric Feb 1, 2026
6431b41
fix: reduce padding in mobile chat input area
google-labs-jules[bot] Feb 1, 2026
8a04d49
Merge pull request #470 from QueueLab/fix/mobile-chat-input-padding-1…
ngoiyaeric Feb 1, 2026
10ab3fe
feat: update Stripe checkout links to new URL
google-labs-jules[bot] Feb 1, 2026
250283e
Merge pull request #471 from QueueLab/update-stripe-links-17354207767…
ngoiyaeric Feb 1, 2026
1b56819
feat: implement GeoJSON upload and tool ingestion pipeline
google-labs-jules[bot] Feb 1, 2026
9f79929
feat: implement GeoJSON upload and tool ingestion pipeline
google-labs-jules[bot] Feb 1, 2026
6554775
fix: ensure long sentences wrap to the next line across UI components
google-labs-jules[bot] Feb 1, 2026
bfe62f9
feat: support pasted GeoJSON and fix refresh loop
google-labs-jules[bot] Feb 1, 2026
9450b15
feat: enable GeoJSON uploads, paste support, and fix refresh loop
google-labs-jules[bot] Feb 1, 2026
e95a25b
Merge pull request #473 from QueueLab/fix/text-overflow-wrapping-4739…
ngoiyaeric Feb 1, 2026
f80c87f
Resolve merge conflicts and synchronize with main
CJWTRUST Feb 2, 2026
c4278e9
feat: final stability and performance improvements
google-labs-jules[bot] Feb 2, 2026
813d264
Resolve merge conflicts and synchronize with main, preserving branch …
google-labs-jules[bot] Feb 2, 2026
c868dcd
Merge branch 'feature/billing-integration-recovery-648258468266820429…
ngoiyaeric Feb 2, 2026
3b5ed27
Resolve merge conflicts: synchronization with main while preserving b…
google-labs-jules[bot] Feb 2, 2026
3984b9b
Restore missing features from commit c4278e9: tenttree usage UI, popu…
CJWTRUST Feb 2, 2026
f45f687
Merge branch 'main' into jules-8488824498232079115-26d4e4cd
ngoiyaeric Feb 2, 2026
67c26d5
Merge pull request #476 from QueueLab/jules-8488824498232079115-26d4e4cd
ngoiyaeric Feb 2, 2026
a842df1
chore: update stripe payment links
google-labs-jules[bot] Feb 2, 2026
184f678
Merge pull request #477 from QueueLab/update-stripe-links-20260202-15…
ngoiyaeric Feb 2, 2026
895bf37
Update UsageView to yearly refresh model
google-labs-jules[bot] Feb 2, 2026
dc345b9
Update usage-view.tsx
ngoiyaeric Feb 2, 2026
86013ed
Update usage-view.tsx
ngoiyaeric Feb 2, 2026
2ba1f9e
Merge pull request #479 from QueueLab/update-usage-view-to-yearly-ref…
ngoiyaeric Feb 2, 2026
dda7a32
fix: standardize header icon spacing and remove unused portals
google-labs-jules[bot] Feb 2, 2026
23a1d3f
Merge pull request #480 from QueueLab/fix-header-icon-spacing-1628392…
ngoiyaeric Feb 2, 2026
885dbbe
feat: make zoom controls conditional on drawing mode
google-labs-jules[bot] Feb 2, 2026
dd812c1
Merge pull request #482 from QueueLab/conditional-zoom-controls-22256…
ngoiyaeric Feb 2, 2026
00c2e1a
Update usage-view.tsx
ngoiyaeric Feb 2, 2026
54d9d6e
Update usage-view.tsx
ngoiyaeric Feb 2, 2026
c9bd9b2
Merge pull request #484 from QueueLab/ngoiyaeric-patch-5
ngoiyaeric Feb 2, 2026
85a85c5
Merge branch 'main' into feature/geojson-upload-pipeline-111249932693…
ngoiyaeric Feb 3, 2026
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
Binary file added CC BY-NC 4.0.docx
Binary file not shown.
117 changes: 111 additions & 6 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
}
}
Comment on lines 215 to 243
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 | 🟠 Major

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 to messageParts at 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
In `@app/actions.tsx` around lines 214 - 241, The pasted GeoJSON block currently
handles the data (using trimmedInput, JSON.parse, aiState.update and
uiStream.append with MapDataUpdater) but does not stop further processing, so
the raw JSON still gets added to messageParts; fix this by short-circuiting
after successful GeoJSON handling — either clear userInput or set a local flag
(e.g. processedGeoJson) and return/skip the subsequent messageParts
construction/send logic so the raw JSON is not appended or sent to the AI model.


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)`
Expand Down Expand Up @@ -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 })
}

Expand All @@ -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
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

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

‼️ 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
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'
}
]
})
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()
// 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'
}
]
})
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 326 - 340, The current branch that creates a
geojson upload message (around geoJson/type check, nanoid generation and
aiState.update that pushes a message with type 'geojson_upload' and content from
file.name) should validate the GeoJSON more thoroughly before updating aiState:
implement or call a lightweight validator (e.g., a new validateGeoJSON function
or an is-geojson/geojson-validation library) to verify
FeatureCollection.features is an array and each Feature has valid properties
(type==='Feature', geometry with type and coordinates) and that
FeatureCollection has valid bbox/crs if present; if validation fails, avoid
pushing the message, log the error (or surface a user-facing error) and return
early. Ensure you reference/replace the simple geoJson.type check with this
validator in the block that generates geoJsonId and calls aiState.update so only
validated GeoJSON gets stored as 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
Copy link

Choose a reason for hiding this comment

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

Server action appends a React client component into the UI stream

submit (a server action) is directly appending <MapDataUpdater /> into uiStream. That means a server-side path is deciding to render a client component as a side effect of file parsing.

This is fragile for a few reasons:

  • It tightly couples server parsing with a specific client synchronization mechanism.
  • It risks odd replay behavior (e.g., streaming replays, retries) causing multiple MapDataUpdater mounts.
  • It makes the UI update dependent on server streaming rather than the persisted AI state reconstruction (which you already added via geojson_upload in getUIStateFromAIState).

Given you already insert a geojson_upload message into aiState, UI reconstruction can naturally render the updater from state without also pushing it immediately.

Suggestion

Remove the immediate uiStream.append(<MapDataUpdater ... />) and rely on the geojson_upload message being rendered via getUIStateFromAIState.

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
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

Silent error handling may confuse users when GeoJSON parsing fails.

When JSON.parse(textContent) fails on Line 325, the error is only logged to the console. The file content still gets prepended to the message (lines 352-357), which may lead to confusing AI responses. Consider providing user feedback:

🛡️ 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
In `@app/actions.tsx` around lines 319 - 350, The GeoJSON parse failure is only
console.logged and the raw file content may still be prepended to messages;
modify the catch block around JSON.parse(textContent) to (1) prevent adding the
MapDataUpdater/uiStream entry or the geojson_upload assistant message when
parsing fails and (2) surface a user-facing error via aiState.update (add an
assistant or system message with id like geoJsonId or similar explaining the
file failed to parse) and optionally append a UI error component instead of
MapDataUpdater; reference the existing symbols JSON.parse, geoJsonId,
aiState.update, uiStream.append, and MapDataUpdater to locate and change
behavior.


const existingTextPart = messageParts.find(p => p.type === 'text')
if (existingTextPart) {
existingTextPart.text = `${textContent}\n\n${existingTextPart.text}`
Expand Down Expand Up @@ -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
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

Map callback does not explicitly return on all paths.

The static analysis tool correctly identifies that the callback passed to map() at line 698 doesn't always return a value. While the early returns at lines 701-703 and 711 handle some cases, the switch statement inside may fall through without returning.

The current code relies on the filter at line 890 to remove undefined values, but explicit returns improve clarity and prevent subtle bugs.

🛡️ 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

‼️ 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
return aiState.messages
.map((message, index) => {
const { role, content, id, type, name } = message
if (role === 'data' && index !== actualLastDataIndex) {
return null
}
return aiState.messages
.map((message, index) => {
const { role, content, id, type, name } = message
if (role === 'data' && index !== actualLastDataIndex) {
return null
}
switch (type) {
// ... switch cases ...
default:
return {
id,
component: null
}
}
return null // Explicit return for unhandled cases within switch
})
.filter(message => message !== null) as UIState
🧰 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
In `@app/actions.tsx` around lines 697 - 703, The map callback over
aiState.messages (inside the function handling message rendering where
actualLastDataIndex is used) does not return on every path — update the switch
inside that callback (the switch handling message.type/name/etc.) to include an
explicit return in the default/fallthrough case (e.g., return null or the
intended fallback JSX/value) so the callback always returns a value and you no
longer rely on later filtering to remove undefined results; ensure you reference
the same variables (message, index, role, content, id, type, name) and keep
existing early returns for role === 'data' and other explicit cases.


if (
!type ||
type === 'end' ||
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

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

Unvalidated JSON.parse for geojson_upload can crash UI rendering

getUIStateFromAIState parses content for geojson_upload without a try/catch. Any malformed content (db corruption, older message format, partial writes) will throw and can break UI state mapping.

You already wrap parsing for other message kinds; this one should be consistent.

Suggestion

Wrap the JSON.parse in a try/catch (similar to the tool and data cases) and return { id, component: null } on failure.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this defensive parsing.

Comment on lines +795 to +801
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

Missing error handling for malformed geojson_upload content.

The JSON.parse call on line 781 will throw if content is malformed, potentially breaking the entire UI state reconstruction. Other similar cases in this function (e.g., tool role at line 790) wrap parsing in try-catch.

🛡️ Proposed fix
             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 }
+              }
             }
📝 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
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.

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

if (
Expand All @@ -735,7 +818,9 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
}
}

const searchResults = createStreamableValue()
const searchResults = createStreamableValue(
JSON.stringify(toolOutput)
)
searchResults.done(JSON.stringify(toolOutput))
switch (name) {
case 'search':
Expand Down Expand Up @@ -775,6 +860,26 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
}
}
break
case 'data':
try {
const contextData = JSON.parse(content as string)
if (contextData.uploadedGeoJson && Array.isArray(contextData.uploadedGeoJson)) {
return {
id,
component: (
<>
{contextData.uploadedGeoJson.map((item: any) => (
<MapDataUpdater key={item.id} id={item.id} data={item.data} filename={item.filename} />
))}
</>
)
}
}
return { id, component: null }
} catch (e) {
console.error('Error parsing data message:', e)
return { id, component: null }
}
Comment on lines +863 to +882
Copy link

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:

  1. Using any here makes it easy to introduce unsafe shapes that still compile. This is UI-state hydration code; it should be strict.
  2. Rendering many MapDataUpdaters will attempt fitBounds for each layer; on chat reload this can cause multiple sequential camera jumps. The current MapDataUpdater only de-dupes by id for inserting into context, but it does not de-dupe the fitBounds side 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
  1. Replace any with a minimal structural type (e.g., { id: string; data: unknown; filename: string }) and validate required fields before rendering.

  2. Add a flag to MapDataUpdater like shouldFlyTo?: boolean and set it to false when replaying persisted uploadedGeoJson from a data message, or add internal logic to only fitBounds on 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.

default:
return {
id,
Expand Down
52 changes: 30 additions & 22 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import { SpeedInsights } from "@vercel/speed-insights/next"
import { Toaster } from '@/components/ui/sonner'
import { MapToggleProvider } from '@/components/map-toggle-context'
import { ProfileToggleProvider } from '@/components/profile-toggle-context'
import { UsageToggleProvider } from '@/components/usage-toggle-context'
import { CalendarToggleProvider } from '@/components/calendar-toggle-context'
import { HistoryToggleProvider } from '@/components/history-toggle-context'
import { HistorySidebar } from '@/components/history-sidebar'
import { MapLoadingProvider } from '@/components/map-loading-context';
import ConditionalLottie from '@/components/conditional-lottie';
import { MapProvider as MapContextProvider } from '@/components/map/map-context'
Expand Down Expand Up @@ -70,28 +73,33 @@ export default function RootLayout({
)}
>
<CalendarToggleProvider>
<MapToggleProvider>
<ProfileToggleProvider>
<ThemeProvider
attribute="class"
defaultTheme="earth"
enableSystem
disableTransitionOnChange
themes={['light', 'dark', 'earth']}
>
<MapContextProvider>
<MapLoadingProvider>
<Header />
<ConditionalLottie />
{children}
<Sidebar />
<Footer />
<Toaster />
</MapLoadingProvider>
</MapContextProvider>
</ThemeProvider>
</ProfileToggleProvider>
</MapToggleProvider>
<HistoryToggleProvider>
<MapToggleProvider>
<ProfileToggleProvider>
<UsageToggleProvider>
<ThemeProvider
attribute="class"
defaultTheme="earth"
enableSystem
disableTransitionOnChange
themes={['light', 'dark', 'earth']}
>
<MapContextProvider>
<MapLoadingProvider>
<Header />
<ConditionalLottie />
{children}
<Sidebar />
<HistorySidebar />
<Footer />
<Toaster />
</MapLoadingProvider>
</MapContextProvider>
</ThemeProvider>
</UsageToggleProvider>
</ProfileToggleProvider>
</MapToggleProvider>
</HistoryToggleProvider>
</CalendarToggleProvider>
<Analytics />
<SpeedInsights />
Expand Down
9 changes: 3 additions & 6 deletions app/search/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
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

🧩 Analysis chain

🏁 Script executed:

cd app/search && find . -type f -name "page.tsx" | head -5

Repository: QueueLab/QCX

Length of output: 71


🏁 Script executed:

cd app && find . -type f -name "*.tsx" -o -name "*.ts" | grep -E "(page|types|message)" | head -20

Repository: 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=tsx

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Check dbMsg source and context
rg -n "dbMsg" app/search/\[id\]/page.tsx -B 5 -A 2

Repository: QueueLab/QCX

Length of output: 607


🏁 Script executed:

rg -n "type AIMessage|interface AIMessage" -A 10

Repository: QueueLab/QCX

Length of output: 1274


🏁 Script executed:

rg -n "type DrizzleMessage|interface DrizzleMessage" -A 10

Repository: 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 -20

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Look for optional chaining or null checks on message fields
rg "message\.(name|type|toolName)" | head -30

Repository: 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 15

Repository: 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 -10

Repository: 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 -3

Repository: QueueLab/QCX

Length of output: 349


🏁 Script executed:

cat -n lib/db/schema.ts

Repository: 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 2

Repository: 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 5

Repository: 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 2

Repository: 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 -50

Repository: 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 2

Repository: QueueLab/QCX

Length of output: 981


Convert nullable database fields to undefined when mapping to AIMessage.

Lines 54–55 cast dbMsg.type and dbMsg.toolName without converting null values to undefined. Since the database schema defines these columns as nullable (no .notNull() constraint) and the AIMessage type expects undefined for optional fields, assign null-coalesced values to maintain type consistency.

Recommended fix
-      type: dbMsg.type as AIMessage['type'],
-      name: dbMsg.toolName as string,
+      type: dbMsg.type ?? undefined,
+      name: dbMsg.toolName ?? undefined,
📝 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
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,
role: dbMsg.role as AIMessage['role'],
content: dbMsg.content,
createdAt: dbMsg.createdAt ? new Date(dbMsg.createdAt) : undefined,
type: dbMsg.type ?? undefined,
name: dbMsg.toolName ?? undefined,
🤖 Prompt for AI Agents
In `@app/search/`[id]/page.tsx around lines 51 - 55, The mapping of DB records to
AIMessage in page.tsx currently casts nullable columns directly (dbMsg.type,
dbMsg.toolName) which can leave nulls where AIMessage expects undefined; update
the mapping in the function that constructs the AIMessage object (the block
assigning role/content/createdAt/type/name) to convert nullable values to
undefined (e.g., use null-coalescing like dbMsg.type ?? undefined and
dbMsg.toolName ?? undefined or conditional expressions) so type and name are
undefined when the DB returns null.

};
});

Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
}
}, 500) // 500ms debounce delay
},
[mapData]
[mapData, setSuggestions]
)

useEffect(() => {
Expand Down Expand Up @@ -214,7 +214,8 @@ export const ChatPanel = forwardRef<ChatPanelRef, ChatPanelProps>(({ messages, i
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="text/plain,image/png,image/jpeg,image/webp"
accept="text/plain,image/png,image/jpeg,image/webp,.geojson,application/geo+json"
data-testid="file-upload-input"
/>
{!isMobile && (
<Button
Expand Down
Loading