-
-
Notifications
You must be signed in to change notification settings - Fork 7
Dual Image Resolution Search and QCX-TERRA Analysis #499
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
Dual Image Resolution Search and QCX-TERRA Analysis #499
Conversation
…re slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan PR Code Suggestions ✨No code suggestions found for the PR. |
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.
No issues can be identified because the diff/content to review was not included. Please provide the patch so I can assess correctness, safety, and maintainability of the dual-image capture flow, CompareSlider UX/state handling, QCX-TERRA analysis trigger, AI-agent changes, and the sonner migration.
Summary of changes
Summary
No code diff was provided in the prompt (the # Diff section is empty), so there are no concrete code changes available to review.
Based on the PR description, the intended changes include:
- Capturing both Mapbox and Google Satellite screenshots during a resolution search.
- Introducing an interactive
CompareSliderto swipe between the two views. - Showing results in a
ResolutionCarouselwith aQCX-TERRA ANALYSISaction for Google imagery. - Updating the AI agent to extract geocoordinates and evaluate COG applicability.
- Migrating notifications from
react-toastifytosonner.
If you share/paste the actual diff (or attach file changes), I can provide a detailed, line-referenced review.
…re slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
WalkthroughAdded dual-source image capture (Mapbox + Google), passed both images and optional lat/lng through submission and resolutionSearch, introduced a ResolutionCarousel + CompareSlider UI, extended resolutionSearch schema/signature (extractedCoordinates, cogInfo, location), added image compression, and migrated toast usage to sonner. Changes
Sequence DiagramsequenceDiagram
participant User
participant HeaderSearchButton
participant MapboxCanvas
participant GoogleStaticAPI
participant AppActions
participant ResolutionAgent
participant ResolutionCarousel
User->>HeaderSearchButton: Click "Analyze"
HeaderSearchButton->>MapboxCanvas: Capture Mapbox canvas (if active)
MapboxCanvas-->>HeaderSearchButton: mapboxBlob
alt Google API key available / Google provider active
HeaderSearchButton->>GoogleStaticAPI: Fetch Google static image
GoogleStaticAPI-->>HeaderSearchButton: googleBlob
end
HeaderSearchButton->>AppActions: Submit FormData (file_mapbox?, file_google?, latitude, longitude, drawnFeatures)
AppActions->>AppActions: Build data URLs, add mapboxImage/googleImage to messages
AppActions->>ResolutionAgent: Call resolutionSearch(messages..., timezone, drawnFeatures?, location)
ResolutionAgent-->>AppActions: Return analysis (includes extractedCoordinates, cogInfo, images)
AppActions->>ResolutionCarousel: Render with mapboxImage, googleImage, initialImage
ResolutionCarousel-->>User: Display slides / CompareSlider / QCX-TERRA actions
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/header-search-button.tsx (1)
135-153:⚠️ Potential issue | 🟠 MajorDon’t disable search for Google provider when Mapbox map is absent.
Line 140 and Line 152 currently block the button whenever
mapis null, even ifmapProvider === 'google'and the flow only needsmapData.cameraState. This prevents Google-based resolution searches on pages without a Mapbox instance.🔧 Suggested fix
- disabled={isAnalyzing || !map || !actions} + disabled={isAnalyzing || !actions || (mapProvider === 'mapbox' && !map)} ... - <Button variant="ghost" size="sm" onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions}> + <Button + variant="ghost" + size="sm" + onClick={handleResolutionSearch} + disabled={isAnalyzing || !actions || (mapProvider === 'mapbox' && !map)} + >app/actions.tsx (1)
203-369: 🛠️ Refactor suggestion | 🟠 MajorRemove the legacy resolution_search block now unreachable after the early return.
The new branch returns at Line 216, so the older resolution_search flow starting at Line 225 will never execute. Keeping both risks divergence and future confusion.
🤖 Fix all issues with AI agents
In `@components/compare-slider.tsx`:
- Around line 41-47: The root div in CompareSlider is interactive but lacks ARIA
semantics and keyboard handling; add proper accessibility by giving the
container div a role="slider", tabindex={0}, and
aria-valuemin/aria-valuemax/aria-valuenow (derived from the component's current
value state) and aria-label or aria-labelledby; implement an onKeyDown handler
(e.g., handleKeyDown) bound to the same element that listens for
ArrowLeft/ArrowRight (and Home/End/PageUp/PageDown if desired) to update the
slider position using the existing update logic (re-use or call the same
internal function that handleMove ultimately drives), and ensure containerRef,
handleMove, and className remain unchanged so keyboard-driven updates mirror
mouse/touch behavior.
In `@components/resolution-carousel.tsx`:
- Around line 31-55: The handleQCXAnalysis function currently proceeds to create
and submit a FormData blob even if fetch(googleImage) fails or returns a non-OK
response; update it to validate the fetch response before converting to blob and
submitting by checking response.ok (and/or response.status) and handling non-OK
responses (e.g., throw or show toast and return) so actions.submit is only
called with a valid blob; references: handleQCXAnalysis, response (from fetch),
blob, and actions.submit.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (7)
app/actions.tsxcomponents/compare-slider.tsxcomponents/header-search-button.tsxcomponents/map/mapbox-map.tsxcomponents/resolution-carousel.tsxlib/agents/resolution-search.tsxresolution_search_block.txt
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-16T10:02:40.220Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-16T10:02:40.220Z
Learning: The router agent and resolution search agent must implement a feedback loop where tool execution results (including GeoTIFF URLs, embeddings, and satellite analysis data) are fed back to the AI model's context through enriched messages before generating the final response, ensuring the foundational model receives and can synthesize all satellite intelligence data.
Applied to files:
resolution_search_block.txtapp/actions.tsxlib/agents/resolution-search.tsx
🧬 Code graph analysis (3)
components/compare-slider.tsx (1)
lib/utils/index.ts (1)
cn(10-12)
app/actions.tsx (7)
lib/agents/resolution-search.tsx (1)
resolutionSearch(41-91)components/map/geojson-layer.tsx (1)
GeoJsonLayer(13-100)components/section.tsx (1)
Section(24-75)components/resolution-carousel.tsx (1)
ResolutionCarousel(26-146)components/message.tsx (1)
BotMessage(11-31)components/resolution-image.tsx (1)
ResolutionImage(22-60)tests/images.spec.ts (1)
test(5-235)
lib/agents/resolution-search.tsx (1)
lib/agents/tools/geospatial.tsx (3)
McpResponse(25-28)Location(18-23)_(147-147)
🪛 Biome (2.3.13)
components/compare-slider.tsx
[error] 42-47: Static Elements should not be interactive.
To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.
(lint/a11y/noStaticElementInteractions)
🔇 Additional comments (4)
components/map/mapbox-map.tsx (1)
8-8: Sonner import update looks good.No behavioral changes needed here.
resolution_search_block.txt (1)
1-170: Resolution search flow wiring looks coherent.Dual-image ingestion and UI updates are consistent throughout the block.
lib/agents/resolution-search.tsx (1)
24-81: Schema + prompt extensions for coordinates/COG are clear.The optional fields and prompt additions are consistent with the new capabilities.
app/actions.tsx (1)
893-904: ResolutionCarousel wiring in UI state looks good.The new props mapping for mapbox/google/initial images is consistent.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| return ( | ||
| <div | ||
| ref={containerRef} | ||
| className={cn("relative w-full aspect-square sm:aspect-video overflow-hidden cursor-ew-resize rounded-lg border bg-muted", className)} | ||
| onMouseMove={handleMove} | ||
| onTouchMove={handleMove} | ||
| > |
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.
Add ARIA + keyboard support for the interactive slider container.
The root div is interactive but lacks semantic role/keyboard handling (Biome a11y rule). This blocks keyboard-only users.
♿ Suggested fix
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
+ const step = event.shiftKey ? 10 : 2
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
+ setSliderPosition(pos => Math.max(0, pos - step))
+ } else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
+ setSliderPosition(pos => Math.min(100, pos + step))
+ }
+ }
+
return (
<div
ref={containerRef}
className={cn("relative w-full aspect-square sm:aspect-video overflow-hidden cursor-ew-resize rounded-lg border bg-muted", className)}
onMouseMove={handleMove}
onTouchMove={handleMove}
+ role="slider"
+ tabIndex={0}
+ aria-label="Compare Mapbox and Google Satellite imagery"
+ aria-valuemin={0}
+ aria-valuemax={100}
+ aria-valuenow={Math.round(sliderPosition)}
+ onKeyDown={handleKeyDown}
>📝 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.
| return ( | |
| <div | |
| ref={containerRef} | |
| className={cn("relative w-full aspect-square sm:aspect-video overflow-hidden cursor-ew-resize rounded-lg border bg-muted", className)} | |
| onMouseMove={handleMove} | |
| onTouchMove={handleMove} | |
| > | |
| const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { | |
| const step = event.shiftKey ? 10 : 2 | |
| if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') { | |
| setSliderPosition(pos => Math.max(0, pos - step)) | |
| } else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') { | |
| setSliderPosition(pos => Math.min(100, pos + step)) | |
| } | |
| } | |
| return ( | |
| <div | |
| ref={containerRef} | |
| className={cn("relative w-full aspect-square sm:aspect-video overflow-hidden cursor-ew-resize rounded-lg border bg-muted", className)} | |
| onMouseMove={handleMove} | |
| onTouchMove={handleMove} | |
| role="slider" | |
| tabIndex={0} | |
| aria-label="Compare Mapbox and Google Satellite imagery" | |
| aria-valuemin={0} | |
| aria-valuemax={100} | |
| aria-valuenow={Math.round(sliderPosition)} | |
| onKeyDown={handleKeyDown} | |
| > |
🧰 Tools
🪛 Biome (2.3.13)
[error] 42-47: Static Elements should not be interactive.
To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.
(lint/a11y/noStaticElementInteractions)
🤖 Prompt for AI Agents
In `@components/compare-slider.tsx` around lines 41 - 47, The root div in
CompareSlider is interactive but lacks ARIA semantics and keyboard handling; add
proper accessibility by giving the container div a role="slider", tabindex={0},
and aria-valuemin/aria-valuemax/aria-valuenow (derived from the component's
current value state) and aria-label or aria-labelledby; implement an onKeyDown
handler (e.g., handleKeyDown) bound to the same element that listens for
ArrowLeft/ArrowRight (and Home/End/PageUp/PageDown if desired) to update the
slider position using the existing update logic (re-use or call the same
internal function that handleMove ultimately drives), and ensure containerRef,
handleMove, and className remain unchanged so keyboard-driven updates mirror
mouse/touch behavior.
| const handleQCXAnalysis = async () => { | ||
| if (!googleImage) return | ||
| setIsAnalyzing(true) | ||
|
|
||
| try { | ||
| const response = await fetch(googleImage) | ||
| const blob = await response.blob() | ||
|
|
||
| setMessages((currentMessages: any[]) => [ | ||
| ...currentMessages, | ||
| { | ||
| id: nanoid(), | ||
| component: <UserMessage content={[{ type: 'text', text: 'Performing QCX-TERRA ANALYSIS on Google Satellite image.' }]} /> | ||
| } | ||
| ]) | ||
|
|
||
| const formData = new FormData() | ||
| formData.append('file', blob, 'google_analysis.png') | ||
| formData.append('action', 'resolution_search') | ||
|
|
||
| const responseMessage = await actions.submit(formData) | ||
| setMessages((currentMessages: any[]) => [...currentMessages, responseMessage as any]) | ||
| } catch (error) { | ||
| console.error('Failed to perform QCX-TERRA ANALYSIS:', error) | ||
| toast.error('An error occurred during analysis.') |
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.
Validate Google image fetch before submitting QCX analysis.
If the fetch fails, the current code still posts the error payload as a file. Add a response check to avoid invalid uploads.
🛠️ Suggested fix
- const response = await fetch(googleImage)
- const blob = await response.blob()
+ const response = await fetch(googleImage)
+ if (!response.ok) {
+ throw new Error(`Failed to fetch Google image (${response.status})`)
+ }
+ const blob = await response.blob()📝 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.
| const handleQCXAnalysis = async () => { | |
| if (!googleImage) return | |
| setIsAnalyzing(true) | |
| try { | |
| const response = await fetch(googleImage) | |
| const blob = await response.blob() | |
| setMessages((currentMessages: any[]) => [ | |
| ...currentMessages, | |
| { | |
| id: nanoid(), | |
| component: <UserMessage content={[{ type: 'text', text: 'Performing QCX-TERRA ANALYSIS on Google Satellite image.' }]} /> | |
| } | |
| ]) | |
| const formData = new FormData() | |
| formData.append('file', blob, 'google_analysis.png') | |
| formData.append('action', 'resolution_search') | |
| const responseMessage = await actions.submit(formData) | |
| setMessages((currentMessages: any[]) => [...currentMessages, responseMessage as any]) | |
| } catch (error) { | |
| console.error('Failed to perform QCX-TERRA ANALYSIS:', error) | |
| toast.error('An error occurred during analysis.') | |
| const handleQCXAnalysis = async () => { | |
| if (!googleImage) return | |
| setIsAnalyzing(true) | |
| try { | |
| const response = await fetch(googleImage) | |
| if (!response.ok) { | |
| throw new Error(`Failed to fetch Google image (${response.status})`) | |
| } | |
| const blob = await response.blob() | |
| setMessages((currentMessages: any[]) => [ | |
| ...currentMessages, | |
| { | |
| id: nanoid(), | |
| component: <UserMessage content={[{ type: 'text', text: 'Performing QCX-TERRA ANALYSIS on Google Satellite image.' }]} /> | |
| } | |
| ]) | |
| const formData = new FormData() | |
| formData.append('file', blob, 'google_analysis.png') | |
| formData.append('action', 'resolution_search') | |
| const responseMessage = await actions.submit(formData) | |
| setMessages((currentMessages: any[]) => [...currentMessages, responseMessage as any]) | |
| } catch (error) { | |
| console.error('Failed to perform QCX-TERRA ANALYSIS:', error) | |
| toast.error('An error occurred during analysis.') |
🤖 Prompt for AI Agents
In `@components/resolution-carousel.tsx` around lines 31 - 55, The
handleQCXAnalysis function currently proceeds to create and submit a FormData
blob even if fetch(googleImage) fails or returns a non-OK response; update it to
validate the fetch response before converting to blob and submitting by checking
response.ok (and/or response.status) and handling non-OK responses (e.g., throw
or show toast and return) so actions.submit is only called with a valid blob;
references: handleQCXAnalysis, response (from fetch), blob, and actions.submit.
…re slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…re slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
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.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@app/actions.tsx`:
- Around line 53-60: The latitude/longitude parsing currently may produce NaN or
out-of-range values and then set location; update the resolution_search branch
(the block using action === 'resolution_search' and variables lat, lng,
location) to parse latitude/longitude into numeric variables, then validate with
Number.isFinite(lat) && Number.isFinite(lng) and range checks (lat between -90
and 90, lng between -180 and 180); only set location = { lat, lng } when those
checks pass, otherwise leave location undefined (or handle as the surrounding
logic expects).
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
app/actions.tsxcomponents/resolution-carousel.tsxcomponents_chat_patch.patch
💤 Files with no reviewable changes (1)
- components_chat_patch.patch
🧰 Additional context used
🧬 Code graph analysis (1)
components/resolution-carousel.tsx (8)
app/actions.tsx (1)
AI(581-660)components/user-message.tsx (1)
UserMessage(15-53)components/resolution-image.tsx (1)
ResolutionImage(22-60)components/ui/button.tsx (1)
Button(56-56)components/ui/carousel.tsx (5)
Carousel(257-257)CarouselContent(258-258)CarouselItem(259-259)CarouselPrevious(260-260)CarouselNext(261-261)components/compare-slider.tsx (1)
CompareSlider(12-91)components/search-results-image.tsx (1)
image(66-140)lib/agents/resolution-search.tsx (1)
resolutionSearch(33-79)
🔇 Additional comments (9)
components/resolution-carousel.tsx (3)
61-81: Slides assembly looks solid.The compare + individual + fallback ordering is clear and consistent.
83-103: Single-slide rendering is clean.Good conditional handling for the analysis button on the Google-only slide.
105-145: Carousel rendering is well-structured.Slide types and navigation controls are handled cleanly.
app/actions.tsx (6)
66-71: Data URL generation is consistent.Mapping mapbox/google buffers to base64 mirrors the primary file flow.
104-149: Resolution search pipeline changes look correct.Passing location and sanitizing message content is a good extension.
170-177: Result payload extension is appropriate.Persisting mapbox/Google images enables proper replay and UI reconstruction.
205-214: ResolutionCarousel integration is clean.Props are passed consistently for the initial render.
224-298: Special-case response path still looks consistent.No issues with the updated flow or return shape.
746-757: UI state reconstruction is correct.ResolutionCarousel now mirrors the stored mapbox/Google imagery as expected.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| if (action === 'resolution_search') { | ||
| const file = formData?.get('file') as File; | ||
| const file_mapbox = formData?.get('file_mapbox') as File; | ||
| const file_google = formData?.get('file_google') as File; | ||
| const file = (formData?.get('file') as File) || file_mapbox || file_google; | ||
| const timezone = (formData?.get('timezone') as string) || 'UTC'; | ||
| const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined; | ||
| const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined; | ||
| const location = (lat !== undefined && lng !== undefined) ? { lat, lng } : undefined; |
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.
Validate latitude/longitude before forming location.
Line 58–60 can pass NaN (or out-of-range) values into location, which may break downstream geo logic. Add finite + range checks before setting location.
✅ Suggested fix
- const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined;
- const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined;
- const location = (lat !== undefined && lng !== undefined) ? { lat, lng } : undefined;
+ const lat = formData?.get('latitude') ? parseFloat(formData.get('latitude') as string) : undefined;
+ const lng = formData?.get('longitude') ? parseFloat(formData.get('longitude') as string) : undefined;
+ const location =
+ Number.isFinite(lat) &&
+ Number.isFinite(lng) &&
+ Math.abs(lat as number) <= 90 &&
+ Math.abs(lng as number) <= 180
+ ? { lat: lat as number, lng: lng as number }
+ : undefined;🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 53 - 60, The latitude/longitude parsing
currently may produce NaN or out-of-range values and then set location; update
the resolution_search branch (the block using action === 'resolution_search' and
variables lat, lng, location) to parse latitude/longitude into numeric
variables, then validate with Number.isFinite(lat) && Number.isFinite(lng) and
range checks (lat between -90 and 90, lng between -180 and 180); only set
location = { lat, lng } when those checks pass, otherwise leave location
undefined (or handle as the surrounding logic expects).
…re slider and QCX-TERRA analysis button Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Added `compressImage` utility to resize and compress screenshots before sending to server actions. - Implemented image compression in `HeaderSearchButton` and `ResolutionCarousel`. - Updated 'QCX-TERRA ANALYSIS' button styling to match platform theme using `variant="default"`. - Resolved syntax and type errors in `app/actions.tsx` and `ResolutionCarousel`. - Continued from previous PR to include Mapbox-Google comparison slider and carousel. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
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.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/header-search-button.tsx (2)
150-171:⚠️ Potential issue | 🔴 CriticalButton is always disabled when using the Google Maps provider.
Both the desktop (Line 155) and mobile (Line 167) buttons include
!mapin theirdisabledcondition. Themapref is the Mapbox map instance, which will benullwhen the user has switched to the Google provider. This makes the resolution search button permanently disabled for Google Maps users, even thoughhandleResolutionSearch(Line 96) has a valid code path for the Google provider.🐛 Proposed fix
disabled={isAnalyzing || !map || !actions}should account for the active provider:
- disabled={isAnalyzing || !map || !actions} + disabled={isAnalyzing || (mapProvider === 'mapbox' && !map) || !actions}Apply the same fix to both the desktop button (Line 155) and mobile button (Line 167).
104-104:⚠️ Potential issue | 🟡 MinorUnclamped zoom level for Google Static Maps.
Math.round(Math.log2(40000000 / (range || 1)))can produce values outside Google Static Maps' supported range (0–21). For instance, a very smallrangeyields zoom > 21; a very largerangecould go negative.🛡️ Proposed fix
- const zoom = Math.round(Math.log2(40000000 / (range || 1))); + const zoom = Math.min(21, Math.max(0, Math.round(Math.log2(40000000 / (range || 1)))));
🤖 Fix all issues with AI agents
In `@components/header-search-button.tsx`:
- Around line 124-128: The file names used when appending image blobs to
FormData (in the mapboxBlob/googleBlob handling and the fallback 'file')
incorrectly use .png while compressImage returns image/jpeg; update the
FormData.append calls for mapboxBlob, googleBlob and the fallback 'file' in this
component (references: mapboxBlob, googleBlob, formData.append('file', ...)) to
use the correct extension by either switching the hardcoded names to .jpg or
dynamically deriving the extension from each blob's MIME type (blob.type) before
calling formData.append so filename and MIME match.
In `@components/resolution-carousel.tsx`:
- Around line 27-28: The cast to any in ResolutionCarousel loses type safety;
replace "as any" by a proper actions interface: either import and reuse the
existing HeaderActions (from header-search-button.tsx) or declare a
ResolutionActions interface matching the actions used in ResolutionCarousel,
then cast useActions<typeof AI>() to that interface (update the const actions =
useActions<typeof AI>() as ResolutionActions/HeaderActions). Ensure the
interface includes the specific methods/properties referenced inside
ResolutionCarousel so TypeScript can validate usages.
- Around line 52-54: handleQCXAnalysis is creating FormData with only 'file' and
'action', which causes the server-side resolutionSearch to default to UTC and
empty drawn-features; update the handleQCXAnalysis FormData to also append the
current 'timezone' and 'drawnFeatures' values (matching the keys used by the
initial resolution search) by reading them from the component's props/state or
the same source header-search-button uses so the re-analysis preserves timezone
and drawn-feature context expected by resolutionSearch.
In `@lib/utils/image-utils.ts`:
- Around line 11-21: The current scaling only reduces the dominant dimension and
can leave the other dimension > max; change the logic to compute a single scale
= Math.min(maxWidth/width, maxHeight/height, 1) and apply it to both width and
height (width = Math.round(width * scale); height = Math.round(height * scale));
then set the canvas.width and canvas.height to these rounded integers so the
canvas uses exact integer dimensions. Update the code that references
width/height (the variables width, height, maxWidth, maxHeight and the canvas
width/height assignments) to use this new scale and rounding.
- Line 49: The blob URL created with URL.createObjectURL(blob) in compressImage
(where img.src is set) must be saved to a local variable (e.g., objectUrl) and
revoked with URL.revokeObjectURL(objectUrl) in both the img.onload and
img.onerror handlers to avoid leaking blob URLs; update the img.src assignment
to use the saved objectUrl and ensure revoke is called after the image is
processed or on error.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
components/header-search-button.tsxcomponents/resolution-carousel.tsxlib/utils/image-utils.ts
🧰 Additional context used
🧬 Code graph analysis (2)
components/resolution-carousel.tsx (7)
app/actions.tsx (1)
AI(581-660)lib/utils/image-utils.ts (1)
compressImage(4-51)components/user-message.tsx (1)
UserMessage(15-53)components/resolution-image.tsx (1)
ResolutionImage(22-60)components/ui/carousel.tsx (11)
Carousel(257-257)CarouselContent(258-258)CarouselItem(259-259)CarouselPrevious(260-260)CarouselNext(261-261)props(178-194)props(156-172)props(49-149)props(229-252)props(200-223)api(101-107)components/compare-slider.tsx (1)
CompareSlider(12-91)lib/agents/resolution-search.tsx (1)
resolutionSearch(33-79)
components/header-search-button.tsx (4)
lib/utils/image-utils.ts (1)
compressImage(4-51)components/map/google-map.tsx (1)
GoogleMapComponent(12-86)components/map/map-query-handler.tsx (2)
prevData(39-48)toolOutput(33-73)components/map/mapbox-map.tsx (3)
prevData(167-167)prevData(347-356)map(67-168)
🔇 Additional comments (5)
components/header-search-button.tsx (3)
76-95: Missingresponse.okcheck on the Google Static Map fetch.When fetching the Google Static Map during a Mapbox session (Line 85),
response.okis checked — good. However,response.blob()on a non-OK response would still produce a blob containing an error HTML/JSON body. This is already handled. Looks correct on re-read.Actually, re-reading: Line 85 does check
if (response.ok). This is fine.
39-47: Early-return guard and pre-checks look good.The null checks for
map(when Mapbox) andactionswith user-friendly toast messages are well-structured.
60-74: Mapbox canvas capture and compression with graceful fallback is solid.The pattern of capturing, compressing, and falling back to the raw blob on compression failure is a good resilience approach.
components/resolution-carousel.tsx (2)
66-86: Slide-building logic is clear and handles all combinations correctly.The fallback chain (both → individual → initial → null) is well-structured. The guard at Line 86 returning
nullwhen no slides are built is a clean no-op.
110-149: Carousel rendering with conditional navigation looks good.Navigation buttons are correctly gated behind
slides.length > 1. The use ofCompareSliderfor the comparison slide andResolutionImagefor individual slides composes the existing components well.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| if (mapboxBlob) formData.append('file_mapbox', mapboxBlob, 'mapbox_capture.png') | ||
| if (googleBlob) formData.append('file_google', googleBlob, 'google_capture.png') | ||
|
|
||
| // Keep 'file' for backward compatibility if needed, or just use the first available | ||
| formData.append('file', (mapboxBlob || googleBlob)!, 'map_capture.png') |
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.
File extension says .png but compressed blob is JPEG.
compressImage outputs image/jpeg blobs, but the filenames passed to FormData.append use .png extensions (mapbox_capture.png, google_capture.png, map_capture.png). If server-side logic infers MIME type from the filename, this mismatch could cause issues. Use .jpg instead, or determine the extension based on whether compression succeeded or fell back to the raw PNG blob.
🤖 Prompt for AI Agents
In `@components/header-search-button.tsx` around lines 124 - 128, The file names
used when appending image blobs to FormData (in the mapboxBlob/googleBlob
handling and the fallback 'file') incorrectly use .png while compressImage
returns image/jpeg; update the FormData.append calls for mapboxBlob, googleBlob
and the fallback 'file' in this component (references: mapboxBlob, googleBlob,
formData.append('file', ...)) to use the correct extension by either switching
the hardcoded names to .jpg or dynamically deriving the extension from each
blob's MIME type (blob.type) before calling formData.append so filename and MIME
match.
| export function ResolutionCarousel({ mapboxImage, googleImage, initialImage }: ResolutionCarouselProps) { | ||
| const actions = useActions<typeof AI>() as any |
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
Prefer a typed interface over as any for actions.
header-search-button.tsx defines a HeaderActions interface and uses it for the cast. Here, as any discards all type safety. Consider reusing or sharing the same interface for consistency.
♻️ Proposed fix
+interface CarouselActions {
+ submit: (formData: FormData) => Promise<any>;
+}
+
export function ResolutionCarousel({ mapboxImage, googleImage, initialImage }: ResolutionCarouselProps) {
- const actions = useActions<typeof AI>() as any
+ const actions = useActions<typeof AI>() as unknown as CarouselActions🤖 Prompt for AI Agents
In `@components/resolution-carousel.tsx` around lines 27 - 28, The cast to any in
ResolutionCarousel loses type safety; replace "as any" by a proper actions
interface: either import and reuse the existing HeaderActions (from
header-search-button.tsx) or declare a ResolutionActions interface matching the
actions used in ResolutionCarousel, then cast useActions<typeof AI>() to that
interface (update the const actions = useActions<typeof AI>() as
ResolutionActions/HeaderActions). Ensure the interface includes the specific
methods/properties referenced inside ResolutionCarousel so TypeScript can
validate usages.
| const formData = new FormData() | ||
| formData.append('file', blob, 'google_analysis.png') | ||
| formData.append('action', 'resolution_search') |
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.
🧩 Analysis chain
🏁 Script executed:
# First, let's find the server-side action handlers and understand how timezone/drawnFeatures are used
rg -n 'resolutionSearch' --type=ts --type=tsx -g '!node_modules/**' | head -20Repository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Search for timezone and drawnFeatures usage in server actions
rg -n 'timezone|drawnFeatures' --type=ts -C 2 -g '!node_modules/**' -g '**/app/actions*' | head -100Repository: QueueLab/QCX
Length of output: 1593
🏁 Script executed:
# Check the header-search-button.tsx file to see lines 131-132
fd -t f 'header-search-button.tsx' | xargs cat -n | grep -A 5 -B 5 '^.*13[0-2]'Repository: QueueLab/QCX
Length of output: 851
🏁 Script executed:
# Search for FormData append patterns to understand what fields are typically sent
rg -n "FormData\(\)" --type=tsx --type=ts -A 5 | head -50Repository: QueueLab/QCX
Length of output: 83
QCX-TERRA re-analysis loses timezone and drawn-feature context.
The handleQCXAnalysis FormData only includes file and action, but omits timezone and drawnFeatures that are sent in the initial resolution search (header-search-button.tsx lines 131-132). The server-side resolutionSearch handler actively uses both parameters (app/actions.tsx lines 45, 57, 104), so the re-analysis will default to UTC and an empty drawn-features array instead of preserving the original analysis context. If this re-analysis is intended to be a fresh, context-free pass on the Google image, this is acceptable — but if it should maintain the original analysis depth, these fields should be propagated.
🤖 Prompt for AI Agents
In `@components/resolution-carousel.tsx` around lines 52 - 54, handleQCXAnalysis
is creating FormData with only 'file' and 'action', which causes the server-side
resolutionSearch to default to UTC and empty drawn-features; update the
handleQCXAnalysis FormData to also append the current 'timezone' and
'drawnFeatures' values (matching the keys used by the initial resolution search)
by reading them from the component's props/state or the same source
header-search-button uses so the re-analysis preserves timezone and
drawn-feature context expected by resolutionSearch.
| if (width > height) { | ||
| if (width > maxWidth) { | ||
| height *= maxWidth / width; | ||
| width = maxWidth; | ||
| } | ||
| } else { | ||
| if (height > maxHeight) { | ||
| width *= maxHeight / height; | ||
| height = maxHeight; | ||
| } | ||
| } |
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.
Scaling logic can exceed maxHeight (or maxWidth) for non-dominant dimensions.
The code only scales the larger dimension. When maxWidth and maxHeight define a different aspect ratio than the source, the non-scaled dimension can still exceed its limit. For example, a 2000×1500 image with maxWidth=1024, maxHeight=512 produces 1024×768, violating maxHeight.
A standard approach is to compute the scale factor for both dimensions and pick the smaller one:
♻️ Proposed fix
- if (width > height) {
- if (width > maxWidth) {
- height *= maxWidth / width;
- width = maxWidth;
- }
- } else {
- if (height > maxHeight) {
- width *= maxHeight / height;
- height = maxHeight;
- }
- }
+ const scale = Math.min(1, maxWidth / width, maxHeight / height);
+ width = Math.round(width * scale);
+ height = Math.round(height * scale);Using Math.round also addresses the floating-point canvas dimension issue (Lines 23-24) — canvas width/height are integers, and setting them to floats causes implicit truncation.
📝 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.
| if (width > height) { | |
| if (width > maxWidth) { | |
| height *= maxWidth / width; | |
| width = maxWidth; | |
| } | |
| } else { | |
| if (height > maxHeight) { | |
| width *= maxHeight / height; | |
| height = maxHeight; | |
| } | |
| } | |
| const scale = Math.min(1, maxWidth / width, maxHeight / height); | |
| width = Math.round(width * scale); | |
| height = Math.round(height * scale); |
🤖 Prompt for AI Agents
In `@lib/utils/image-utils.ts` around lines 11 - 21, The current scaling only
reduces the dominant dimension and can leave the other dimension > max; change
the logic to compute a single scale = Math.min(maxWidth/width, maxHeight/height,
1) and apply it to both width and height (width = Math.round(width * scale);
height = Math.round(height * scale)); then set the canvas.width and
canvas.height to these rounded integers so the canvas uses exact integer
dimensions. Update the code that references width/height (the variables width,
height, maxWidth, maxHeight and the canvas width/height assignments) to use this
new scale and rounding.
| }; | ||
|
|
||
| img.onerror = () => reject(new Error('Failed to load image for compression')); | ||
| img.src = URL.createObjectURL(blob); |
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.
Memory leak: Object URL is never revoked.
URL.createObjectURL(blob) allocates a blob URL that persists until explicitly revoked or the document is unloaded. Since compressImage can be called multiple times per session (every capture/compress cycle), each call leaks a blob URL reference.
Revoke the URL in both the onload and onerror callbacks.
🔧 Proposed fix
img.onload = () => {
+ URL.revokeObjectURL(img.src);
let width = img.width;
let height = img.height;
// ...
};
- img.onerror = () => reject(new Error('Failed to load image for compression'));
+ img.onerror = () => {
+ URL.revokeObjectURL(img.src);
+ reject(new Error('Failed to load image for compression'));
+ };
img.src = URL.createObjectURL(blob);📝 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.
| img.src = URL.createObjectURL(blob); | |
| img.onload = () => { | |
| URL.revokeObjectURL(img.src); | |
| let width = img.width; | |
| let height = img.height; | |
| // ... | |
| }; | |
| img.onerror = () => { | |
| URL.revokeObjectURL(img.src); | |
| reject(new Error('Failed to load image for compression')); | |
| }; | |
| img.src = URL.createObjectURL(blob); |
🤖 Prompt for AI Agents
In `@lib/utils/image-utils.ts` at line 49, The blob URL created with
URL.createObjectURL(blob) in compressImage (where img.src is set) must be saved
to a local variable (e.g., objectUrl) and revoked with
URL.revokeObjectURL(objectUrl) in both the img.onload and img.onerror handlers
to avoid leaking blob URLs; update the img.src assignment to use the saved
objectUrl and ensure revoke is called after the image is processed or on error.
|
@coderabbitai the chat input does not respond |
|
@coderabbitai or I think its taking too long and the inquiry text is out of bounds for the UI |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain
🏁 Script executed: Length of output: 7375 🏁 Script executed: Length of output: 2321 🏁 Script executed: Length of output: 1708 🏁 Script executed: Length of output: 9978 🏁 Script executed: Length of output: 905
Root CauseWhen a suggestion is selected or the EmptyScreen's submitMessage is called:
The original SolutionHere's the fix for // In the SuggestionsDropdown onSelect handler (around line 112-116)
onSelect={query => {
setInput(query)
setSuggestions(null)
// Use a small timeout to ensure state update before submission
setTimeout(() => {
setIsSubmitting(true)
}, 0)
}}
// In both EmptyScreen submitMessage handlers (lines 154-157 and 193-196)
submitMessage={message => {
setInput(message)
setTimeout(() => {
setIsSubmitting(true)
}, 0)
}}Alternatively, for a cleaner solution, you could modify ChatPanel to accept an optional 💡 Use |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain
🏁 Script executed: Length of output: 7375 🏁 Script executed: Length of output: 27 🏁 Script executed: Length of output: 335 🏁 Script executed: Length of output: 6666 🏁 Script executed: Length of output: 9978 🏁 Script executed: Length of output: 4239 🏁 Script executed: Length of output: 59
1. Race Condition: Chat Input Not RespondingReact processes state updates after event handlers have finished running in a process called batching. The patch removed In onSelect={query => {
setInput(query) // State update is queued
setSuggestions(null)
setIsSubmitting(true) // Immediately triggers submission
}}The problem: Solution: Restore the onSelect={query => {
setInput(query)
setSuggestions(null)
// Defer submission to next tick to allow state to settle
setTimeout(() => setIsSubmitting(true), 0)
}}Apply the same fix to the submitMessage={message => {
setInput(message)
setTimeout(() => setIsSubmitting(true), 0)
}}2. Text Overflow in UIThe Textarea in Solution: Add overflow control classes to the Textarea: className={cn(
'resize-none w-full min-h-12 rounded-fill border border-input pl-14 pr-12 pt-3 pb-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'break-words overflow-wrap-anywhere', // Add these overflow classes
isMobile
? 'mobile-chat-input input bg-background'
: 'bg-muted'
)}These changes will fix both the input responsiveness and text overflow issues. Would you like me to create an issue to track these fixes? |
|
@coderabbitai the input chat for follow up after resolution search does not input any text, there is no response. Also the recommended queries on the on the chat input is displaying even after response. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 7375 🏁 Script executed: Length of output: 5148 🏁 Script executed: Length of output: 3335 🏁 Script executed: Length of output: 905 🏁 Script executed: Length of output: 6537
1. Missing setTimeout (Race Condition)The previous fix wasn't fully applied. In Current code (line 112-117): onSelect={query => {
setInput(query)
setSuggestions(null)
// Use a small timeout to ensure state update before submission
setIsSubmitting(true) // ❌ This fires immediately!
}}Should be: onSelect={query => {
setInput(query)
setSuggestions(null)
// Use a small timeout to ensure state update before submission
setTimeout(() => {
setIsSubmitting(true)
}, 0)
}}The same issue exists in the EmptyScreen handler (line 154-157): submitMessage={message => {
setInput(message)
setTimeout(() => {
setIsSubmitting(true)
}, 0)
}}2. Suggestions Not Cleared After ResponseWhen new messages arrive (like after resolution search completes), the suggestions dropdown should be hidden but there's no code doing this. Add this useEffect in useEffect(() => {
setShowEmptyScreen(messages.length === 0)
}, [messages])
// Add this new effect:
useEffect(() => {
// Clear suggestions whenever messages change
if (messages.length > 0) {
setSuggestions(null)
}
}, [messages])This ensures that when ResolutionCarousel (or any other component) adds new messages, the suggestions dropdown is automatically cleared. Summary of ChangesFile:
These two fixes will:
Would you like me to create a plan to implement these changes? |
@coderabbitai the suggested prompts are being displayed even after response
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 7375
Root CauseWhen new messages arrive (like after a resolution search response), nothing clears the Fix RequiredIn useEffect(() => {
setShowEmptyScreen(messages.length === 0)
}, [messages])
// Add this new effect to clear suggestions when messages arrive:
useEffect(() => {
if (messages.length > 0) {
setSuggestions(null)
}
}, [messages])This ensures that whenever the Additional Issue to FixWhile you're editing this file, also fix line 115 where the setTimeout is commented but not implemented: Current (line 112-117): onSelect={query => {
setInput(query)
setSuggestions(null)
// Use a small timeout to ensure state update before submission
setIsSubmitting(true) // ❌ Should be wrapped in setTimeout
}}Should be: onSelect={query => {
setInput(query)
setSuggestions(null)
// Use a small timeout to ensure state update before submission
setTimeout(() => {
setIsSubmitting(true)
}, 0)
}}These two changes will: Would you like me to create a plan to implement both fixes? |
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.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
components/chat.tsx (1)
52-61: 🧹 Nitpick | 🔵 TrivialConsider merging the two
useEffecthooks that share the same[messages]dependency.Lines 52–54 and 57–61 both react to
messageschanges. They can be combined into a single effect for clarity.♻️ Proposed merge
- useEffect(() => { - setShowEmptyScreen(messages.length === 0) - }, [messages]) - - // Clear suggestions when a response is received or messages change - useEffect(() => { - if (messages.length > 0) { - setSuggestions(null) - } - }, [messages]) + useEffect(() => { + setShowEmptyScreen(messages.length === 0) + if (messages.length > 0) { + setSuggestions(null) + } + }, [messages])components/chat-panel.tsx (1)
235-278:⚠️ Potential issue | 🟡 MinorTextarea lacks overflow-wrap handling — long unbroken text will overflow.
As noted in PR comments, the
Textareahas nobreak-wordsoroverflow-wrapstyling. Long URLs or unbroken strings will overflow the input container.🛠️ Proposed fix
className={cn( - 'resize-none w-full min-h-12 rounded-fill border border-input pl-14 pr-12 pt-3 pb-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', + 'resize-none w-full min-h-12 rounded-fill border border-input pl-14 pr-12 pt-3 pb-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 break-words [overflow-wrap:anywhere]',app/actions.tsx (2)
140-150:⚠️ Potential issue | 🟡 Minor
sanitizedHistoryis computed but never used — dead code.
sanitizedHistoryreplaces image parts with"IMAGE_PROCESSED"placeholders but is never referenced after construction. OnlysanitizedMessages(line 129) is passed toquerySuggestoron line 151.🧹 Remove dead code
- const sanitizedHistory = currentMessages.map((m: any) => { - if (m.role === "user" && Array.isArray(m.content)) { - return { - ...m, - content: m.content.map((part: any) => - part.type === "image" ? { ...part, image: "IMAGE_PROCESSED" } : part - ) - } - } - return m - });
160-194:⚠️ Potential issue | 🟠 MajorStoring full base64 data URLs for up to three images in AI state risks significant storage bloat.
Lines 175–177 embed
dataUrl,mapboxDataUrl, andgoogleDataUrl(all base64-encoded screenshots) inside theresolution_search_resultmessage content. This JSON blob is persisted to the database viaonSetAIState→saveChat. Each screenshot can be hundreds of KB to several MB in base64, so a single resolution search could add multiple megabytes to the stored chat. Over time this will degrade database performance and increase storage costs.Consider storing images in object storage (e.g., S3/R2) and saving only the URL references in AI state, or at minimum compress/resize before base64-encoding.
🤖 Fix all issues with AI agents
In `@app/actions.tsx`:
- Around line 54-56: The current fallback logic sets file =
(formData?.get('file') as File) || file_mapbox || file_google, which causes
dataUrl to duplicate mapboxDataUrl when file_mapbox is used and results in
storing the same base64 twice (image and mapboxImage). Fix by deriving a single
primaryDataUrl based on which source was actually chosen: if file ===
file_mapbox use mapboxDataUrl as primaryDataUrl (avoid re-reading/encoding the
same file), otherwise compute dataUrl for the chosen file; then store
primaryDataUrl in the result.image and keep mapboxDataUrl only in
result.mapboxImage. Make the change around the variables file, file_mapbox,
file_google, dataUrl, mapboxDataUrl, image and mapboxImage to ensure no
duplicate base64 strings are produced.
- Around line 231-234: The product definition strings are hardcoded in the
server action (variable definition computed from userInput) making marketing
copy brittle; extract both product descriptions and their pricing URLs into a
configuration source (e.g., a constants/config object or CMS-backed content) and
replace the inline literals in the userInput handling logic with references to
those keys; update the branch that sets definition (the conditional using
userInput.toLowerCase().trim() and the variable definition) to look up the
appropriate config entry (e.g., PRODUCT_DEFINITIONS['planet_computer'] or via a
content service) so copy and links can be edited without changing
app/actions.tsx.
In `@components/resolution-carousel.tsx`:
- Line 53: The file name and MIME/type are mismatched: compressImage (via
canvas.toBlob with 'image/jpeg') produces a JPEG blob but the code appends it as
'google_analysis.png' in formData.append; update the formData append to use a
matching filename/extension (e.g., 'google_analysis.jpg') or derive the
extension from the blob.type before calling formData.append('file', blob,
filename) so the appended filename matches the blob MIME (refer to
compressImage, formData.append('file', blob, 'google_analysis.png') and the
canvas.toBlob('image/jpeg') usage).
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
app/actions.tsxcomponents/chat-panel.tsxcomponents/chat.tsxcomponents/resolution-carousel.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
components/resolution-carousel.tsx (6)
app/actions.tsx (1)
AI(582-661)lib/utils/image-utils.ts (1)
compressImage(4-51)components/user-message.tsx (1)
UserMessage(15-53)components/resolution-image.tsx (1)
ResolutionImage(22-60)components/compare-slider.tsx (1)
CompareSlider(12-91)lib/agents/resolution-search.tsx (1)
resolutionSearch(33-79)
app/actions.tsx (1)
lib/agents/resolution-search.tsx (3)
resolutionSearch(41-91)resolutionSearch(33-79)DrawnFeature(26-31)
🔇 Additional comments (3)
components/chat.tsx (1)
119-124: ThesetInput→setIsSubmittingsequence relies on React batching — confirm this works reliably.
setInput(query)followed immediately bysetIsSubmitting(true)depends on React 18+ batching both updates into a single render, after which theuseEffectonisSubmitting(line 96) firessubmitForm. Because theChatPanelreceivesinputas a prop andhandleSubmitreads that prop, the sequencing should be correct under React 18+ automatic batching. The PR comments flagged a race condition here from a prior version that usedsetTimeout; the current approach with theuseEffectintermediary is functionally equivalent and avoids the race.components/chat-panel.tsx (1)
105-105: Good addition — clearing suggestions on submit prevents stale dropdown state.app/actions.tsx (1)
743-758:getUIStateFromAIStatecorrectly reconstructsResolutionCarouselfrom stored image fields.The mapping reads
mapboxImageandgoogleImagefrom the parsed analysis result and passes them toResolutionCarouselalongsideinitialImage. This aligns with the storage format established in thesubmitfunction.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| const file_mapbox = formData?.get('file_mapbox') as File; | ||
| const file_google = formData?.get('file_google') as File; | ||
| const file = (formData?.get('file') as File) || file_mapbox || file_google; |
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.
File fallback creates redundant data: when file falls back to file_mapbox, dataUrl duplicates mapboxDataUrl.
Line 56 falls back: const file = (formData?.get('file') as File) || file_mapbox || file_google. If no explicit file is provided, file becomes file_mapbox, and line 74 converts it to dataUrl — which is identical to mapboxDataUrl (line 68). The result JSON then stores the same base64 string twice under image and mapboxImage, doubling the storage cost for that image.
♻️ Suggested approach
const buffer = await file.arrayBuffer();
const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`;
+ // Avoid duplicate: if file fell back to mapbox/google, reuse that data URL
+ const primaryDataUrl = (file === file_mapbox && mapboxDataUrl)
+ ? mapboxDataUrl
+ : (file === file_google && googleDataUrl)
+ ? googleDataUrl
+ : dataUrl;Then use primaryDataUrl as the image field in the stored result instead of dataUrl.
Also applies to: 67-74
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 54 - 56, The current fallback logic sets file =
(formData?.get('file') as File) || file_mapbox || file_google, which causes
dataUrl to duplicate mapboxDataUrl when file_mapbox is used and results in
storing the same base64 twice (image and mapboxImage). Fix by deriving a single
primaryDataUrl based on which source was actually chosen: if file ===
file_mapbox use mapboxDataUrl as primaryDataUrl (avoid re-reading/encoding the
same file), otherwise compute dataUrl for the chosen file; then store
primaryDataUrl in the result.image and keep mapboxDataUrl only in
result.mapboxImage. Make the change around the variables file, file_mapbox,
file_google, dataUrl, mapboxDataUrl, image and mapboxImage to ensure no
duplicate base64 strings are produced.
| if (userInput && (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)` | ||
|
|
||
| : `QCX-Terra is a model garden of pixel level precision geospatial foundational models for efficient land feature predictions from satellite imagery. Available for our Pro and Enterprise customers. [QCX Pricing] (https://www.queue.cx/#pricing)`; |
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
Hardcoded product definition strings with inline marketing copy are fragile and difficult to maintain.
These definitions (lines 231–234) are hardcoded with pricing links and product descriptions. Consider moving them to a configuration file or CMS to keep marketing content out of server action logic.
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 231 - 234, The product definition strings are
hardcoded in the server action (variable definition computed from userInput)
making marketing copy brittle; extract both product descriptions and their
pricing URLs into a configuration source (e.g., a constants/config object or
CMS-backed content) and replace the inline literals in the userInput handling
logic with references to those keys; update the branch that sets definition (the
conditional using userInput.toLowerCase().trim() and the variable definition) to
look up the appropriate config entry (e.g.,
PRODUCT_DEFINITIONS['planet_computer'] or via a content service) so copy and
links can be edited without changing app/actions.tsx.
| ]) | ||
|
|
||
| const formData = new FormData() | ||
| formData.append('file', blob, 'google_analysis.png') |
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.
Filename/MIME-type mismatch: compressed blob is JPEG but named .png.
compressImage (from lib/utils/image-utils.ts) produces a JPEG blob via canvas.toBlob(..., 'image/jpeg', quality), but the file is appended as 'google_analysis.png'. This could confuse downstream consumers expecting the extension to match the actual format.
🛠️ Proposed fix
- formData.append('file', blob, 'google_analysis.png')
+ formData.append('file', blob, 'google_analysis.jpg')📝 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.
| formData.append('file', blob, 'google_analysis.png') | |
| formData.append('file', blob, 'google_analysis.jpg') |
🤖 Prompt for AI Agents
In `@components/resolution-carousel.tsx` at line 53, The file name and MIME/type
are mismatched: compressImage (via canvas.toBlob with 'image/jpeg') produces a
JPEG blob but the code appends it as 'google_analysis.png' in formData.append;
update the formData append to use a matching filename/extension (e.g.,
'google_analysis.jpg') or derive the extension from the blob.type before calling
formData.append('file', blob, filename) so the appended filename matches the
blob MIME (refer to compressImage, formData.append('file', blob,
'google_analysis.png') and the canvas.toBlob('image/jpeg') usage).
290929a to
66ae964
Compare

This pull request implements the dual image resolution search feature. It captures both Mapbox and Google Satellite screenshots when a user triggers a resolution search. A new interactive CompareSlider allows users to swipe between these views. The results are displayed in a ResolutionCarousel, which includes a 'QCX-TERRA ANALYSIS' button for further processing of Google images. The AI agent has also been updated to extract geocoordinates and evaluate COG applicability. Additionally, the project has been updated to use 'sonner' for all notifications, replacing 'react-toastify'.
PR created automatically by Jules for task 14866169086758664624 started by @ngoiyaeric
Summary by CodeRabbit
New Features
Improvements
Bug Fixes / UX