Merge Resolution Search Enhancement (f89e3f7) into Main#490
Merge Resolution Search Enhancement (f89e3f7) into Main#490ngoiyaeric wants to merge 46 commits intomainfrom
Conversation
Restored the branch to the expected head at commit 488b47c. This recovers several missing features and architectural improvements: - Integrated HistoryToggleProvider and HistorySidebar. - Integrated UsageToggleProvider and the new UsageView component. - Removed legacy usage-sidebar.tsx. - Restored pricing and feature updates in PurchaseCreditsPopup. - Fixed layout structure in app/layout.tsx to support these new global state providers. Resolved previous merge conflicts and incorrect force-push state. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…c380 Restore Branch Head to Commit 488b47c
- Reset branch HEAD to historical recovery point 488b47c. - Applied latest code fixes from orphan commit 166aee9, including Mapbox rendering optimizations and GeoJSON streaming. - Improved type safety in app/actions.tsx by removing 'as any' casts and properly typing message arrays. - Refined getModel utility to support vision-aware model fallback. - Documented non-blocking background task pattern in server actions. - Updated feature/billing-integration branch with the restored state. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Reset branch HEAD to historical recovery point 488b47c. - Applied latest code fixes from orphan commit 166aee9. - Resolved TypeScript build error in app/actions.tsx by casting GeoJSON data to FeatureCollection. - Improved type safety for AIState message filtering and content handling. - Implemented background processing for resolution search with immediate UI streaming. - Optimized chat history fetching to trigger only when the sidebar is open. - Ensured Mapbox style loading is robust against re-renders. - Cleaned up dead code and improved documentation in server actions. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Reset branch HEAD to historical recovery point 488b47c. - Applied latest code fixes from orphan commit 166aee9. - Resolved TypeScript build error in app/actions.tsx by casting GeoJSON data to FeatureCollection. - Improved type safety for AIState message filtering and content handling. - Implemented background processing for resolution search with immediate UI streaming. - Re-enabled auto-opening of the pricing popup in components/header.tsx. - Optimized chat history fetching to trigger only when the sidebar is open. - Ensured Mapbox style loading is robust against re-renders. - Cleaned up dead code and improved documentation in server actions. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Restored historical state from 488b47c and merged latest changes. - Implemented streaming for resolution search in lib/agents/resolution-search.tsx and app/actions.tsx to improve response time. - Re-enabled auto-opening pricing popup in components/header.tsx. - Resolved all TypeScript build errors in app/actions.tsx. - Restored Grok model support for vision tasks in lib/utils/index.ts. - Optimized chat history loading in components/sidebar/chat-history-client.tsx. - Improved Mapbox style loading robustness in components/map/mapbox-map.tsx. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Synchronized branch with origin/main, prioritizing main's code structure and latest features. - Bridged the history with historical recovery point 488b47c. - Implemented streaming for resolution search in lib/agents/resolution-search.tsx and app/actions.tsx to resolve performance issues. - Restored the auto-opening Pricing Popup and Usage View in the Header component. - Integrated the Timezone Clock and time context into the restored resolution search logic. - Resolved TypeScript build errors with proper type casting and fixed a missing 'use client' directive in components/history.tsx. - Ensured all required providers (History, Usage, etc.) are correctly wrapped in the root layout. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Synchronized branch with origin/main, prioritizing main's code structure and latest features. - Bridged the history with historical recovery point 488b47c. - Integrated UsageView and billing UI into the Chat component, toggled by the tent tree icon. - Implemented streaming for resolution search in lib/agents/resolution-search.tsx and app/actions.tsx for better performance. - Restored the auto-opening Pricing Popup in the Header component. - Improved type safety across server actions and Mapbox components. - Ensured mutual exclusion between Settings, Usage, and Map views. - Fixed a missing 'use client' directive in components/history.tsx. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Integrated UsageView into the main Chat component for both mobile and desktop. - Ensured mutual exclusion between Settings, Usage, and Map views in the UI panel. - Updated ConditionalLottie to hide the loading animation when the Usage View is open. - Synchronized with origin/main while prioritizing its code structure. - Maintained historical recovery and performance optimizations for resolution search. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Synchronized with origin/main, prioritizing main branch code and features. - Fully restored historical context and missing changes from commit 488b47c. - Integrated Usage and Billing UI (UsageView) into the Chat component. - Implemented streaming for resolution search analysis to improve perceived performance. - Re-enabled auto-opening pricing popup in components/header.tsx. - Refined ConditionalLottie visibility to avoid overlaps with the Usage UI. - Ensured mutual exclusion between Settings, Usage, and Map views in the side panel. - Improved type safety across server actions and Mapbox components. - Resolved build failures related to missing client directives and type mismatches. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…ability - Recovered historical state from commit 488b47c and synchronized with main. - Optimized resolution search by refactoring to stream partial summaries. - Fixed Mapbox memory leaks by ensuring all event listeners are removed on cleanup. - Improved StreamableValue stability by initializing with default values. - Integrated Usage View with mutual exclusion logic and Lottie player visibility fixes. - Refined model selection for Grok vision and Gemini 1.5 Pro. - Integrated timezone-aware analysis using tz-lookup. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
- Fixed Type error in `app/actions.tsx` by casting `content` to `string` in `createStreamableValue`. - Addressed ESLint warnings in `components/map/mapbox-map.tsx` and `components/chat-panel.tsx` by adding missing dependencies to `useEffect` and `useCallback` hooks. - Ensured `relatedQueries` streamable value is initialized with an empty state for stability. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
Update LICENSE
- Reduced vertical padding in `.mobile-chat-input-area` in `globals.css`. - Removed redundant `.mobile-chat-input` class and associated styles from `globals.css`. - Adjusted `ChatPanel` to use more compact padding and a smaller minimum height on mobile. - Removed excessive left padding (`pl-14`) on the mobile chat input since the attachment button is moved to the icons bar. - Cleaned up unused mobile-specific CSS classes in `globals.css`. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…8048805505961867280 Reduce padding in mobile chat input area
Replaced all occurrences of the old Stripe checkout link with the new URL: https://buy.stripe.com/14A3cv7K72TR3go14Nasg02 Updated files: - components/mobile-icons-bar.tsx - components/header.tsx Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…683339330 Update Stripe checkout links
- Updated `EmptyScreen` example buttons to use `whitespace-normal`, `text-left`, and `items-start` for better multi-line support. - Added `break-words` to `BotMessage` container to prevent horizontal overflow from long strings. - Improved `SuggestionsDropdown` and `SearchRelated` buttons with wrapping and top-alignment. - Replaced filename truncation with `break-all` wrapping in `ChatPanel` file attachments. - Ensured consistent icon alignment for multi-line buttons. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…911856121500176 Ensure text wrapping for long sentences across UI components
- Optimized resolution search with streaming. - Improved timezone handling and AI context. - Fixed UI mutual exclusion and loading state logic. - Stabilized Mapbox cleanup and event listeners. - Resolved build errors and ESLint warnings. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…head features - Reverted branch to commit 6acfbe5. - Merged changes from main (e95a25b). - Preserved Tentree Icon (UsageView logic), History layout (HistoryContainer), and PurchaseCreditsPopup from 6acfbe5 as requested. - Updated Stripe checkout URL in PurchaseCreditsPopup to the official one (14A3cv7K72TR3go14Nasg02). - Re-applied bug fixes from 6acfbe5 (type casting in app/actions.tsx, ESLint dependencies in components/map/mapbox-map.tsx and components/chat-panel.tsx, and relatedQueries initialization). - Verified build and synchronization with main branch features (e.g., resolution search enhancements). Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…8' into jules-8488824498232079115-26d4e4cd
…ranch head features - Reverted branch to commit 6acfbe5 and merged with main (e95a25b). - Preserved Tentree Icon (Usage View), history layout, and PurchaseCreditsPopup from 6acfbe5. - Restored branch-specific UI components: Header, History button, Profile toggle, and Usage integration in Chat. - Synchronized all other logic with main, including resolution search enhancements. - Fixed an infinite re-render loop in Mapbox map by optimizing useEffect dependencies (removed cameraState from initialization effect). - Re-applied critical bug fixes from 6acfbe5: - Type casting in app/actions.tsx (getUIStateFromAIState). - Streamable value initialization for stability. - setSuggestions dependency in ChatPanel. - Updated Stripe checkout URL in PurchaseCreditsPopup to the official link. - Verified successful build with 'bun run build'. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…p, and history toggle
Jules 8488824498232079115 26d4e4cd
…resh-14763946411330416327 Update Usage View to Yearly Refresh
This commit refactors the desktop header icon container in `components/header.tsx` to ensure all icons are equidistant. Key changes: - Changed the icon container from `justify-between w-1/2` to `justify-center flex-1` with a fixed `gap-10`. - Removed the unused `<div id="timezone-clock-portal" />` which was causing irregular spacing between the TentTree and ModeToggle icons. - Applied `className="contents"` to the `<div id="header-search-portal" />` so that it doesn't affect the flex layout when empty. - These changes ensure that icons are always perfectly centered and equidistant regardless of the number of active icons. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…1736717079589 Standardize Header Icon Spacing
- Modified `mapbox-map.tsx` to only show `NavigationControl` when in `DrawingMode` on desktop. - Fixed a bug in the mode change `useEffect` where previous state was being updated prematurely, preventing cleanup logic from executing correctly. - Updated `tests/map.spec.ts` to switch to `DrawingMode` before verifying zoom control visibility. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…94765400062008 Conditional Map Zoom Controls and Mode Transition Fix
Update usage-view.tsx
- Merged commit f89e3f7 into main history. - Ensured components/header-search-button.tsx correctly includes 'drawnFeatures' in search requests. - Integrated ResolutionImage component for visual feedback in chat. - Refactored SearchRelated and Chat suggestions UI for better interactivity. - Included map drawings in researcher agent context for spatial awareness. 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.
|
|
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (10)
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 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 |
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
|||||||||||||||||||||||||
PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||||||||||||
There was a problem hiding this comment.
Key issues are around persisting base64 image data in chat state (likely to cause serious performance/storage problems) and a few maintainability pitfalls (typoed groupeId, brittle setTimeout submission ordering). ResolutionImage opts out of Next’s image handling via an eslint disable without a clear need, and SearchRelated now lacks in-flight guards and uses unstable keys. The server-side geospatial helper should prefer server-only API keys over NEXT_PUBLIC_* to avoid accidental reliance on client-exposed secrets.
Additional notes (2)
- Maintainability |
app/actions.tsx:87-87
The newgroupeId(typo) is now created early and used for both the assistant response and the GeoJSON layer id. This improves consistency, but naming is a problem:groupeIdis easy to misread and repeated across the file.
This is a maintainability footgun because the id is now used for multiple UI artifacts (message grouping and map layer id), so clarity matters.
- Maintainability |
components/search-related.tsx:58-64
SearchRelatednow useskey={index}and triggers network actions per click. Using the index as key can cause subtle UI issues if the list changes (e.g., streaming updates, reordering). This component is also a perfect place to disable the button while a submit is in-flight to prevent duplicate submissions.
Since you removed pending from useStreamableValue, you currently have no guard against double clicks.
Summary of changes
What this PR changes
-
Adds user-drawn map context to AI requests
components/chat-panel.tsx,components/followup-panel.tsx, andcomponents/header-search-button.tsxnow appenddrawnFeaturestoFormData.app/actions.tsxparsesdrawnFeaturesonce and passes it through toresolutionSearch(...)andresearcher(...).
-
Improves resolution search visual feedback
- New
components/resolution-image.tsxshows a clickable preview + dialog for the captured map image. app/actions.tsxrenders<ResolutionImage src={dataUrl} />alongside the streamed bot response.resolution_search_resultmessages now persist{ ...analysisResult, image: dataUrl }so historical UI can re-render the image.
- New
-
Refactors related query click handling
components/search-related.tsxswitches from a form submit pattern to direct buttononClick, generating aFormDatawithrelated_query.
-
Enhances geospatial tool outputs for Google
lib/agents/tools/geospatial.tsxadds a helper to create Google Static Map URLs and injectsmapUrlinto MCP responses when provider isgoogle.app/actions.tsxshows the returned map preview image aboveMapQueryHandlerwhen available.
-
Incorporates drawn features into the default system prompt
lib/agents/researcher.tsxincludes a summary of drawn features in the default prompt when present.
-
UI suggestion overlay adjustments
components/chat.tsxcentralizes suggestion overlay rendering viarenderSuggestions()and applies blur/pointer-event blocking under the overlay.
| const action = formData?.get('action') as string; | ||
| const drawnFeaturesString = formData?.get('drawnFeatures') as string; | ||
| let drawnFeatures: DrawnFeature[] = []; | ||
| try { | ||
| drawnFeatures = drawnFeaturesString ? JSON.parse(drawnFeaturesString) : []; | ||
| } catch (e) { | ||
| console.error('Failed to parse drawnFeatures:', e); | ||
| } | ||
|
|
There was a problem hiding this comment.
drawnFeatures is parsed at the top-level of submit() even for actions that never need it. More importantly, the value is computed from client-provided JSON without any size/shape guardrails. A large payload (or maliciously large JSON) can increase server work and bloat logs/state.
Even if you trust the source, it’s cheap to add a minimal cap (e.g., max length of the raw string / max number of features) and only parse when needed (or when present).
Suggestion
Consider lazy-parsing drawnFeatures only when required (e.g., when action === 'resolution_search' or when you’re about to call researcher(...)), and add basic guardrails:
- Reject/ignore if
drawnFeaturesString.lengthexceeds a reasonable threshold (e.g., 50–200KB) - After parsing, clamp
drawnFeatures.length(e.g., max 200) and drop oversizedmeasurementstrings
Example sketch:
const rawDrawn = (formData?.get('drawnFeatures') as string) ?? ''
const shouldParse = action === 'resolution_search' || rawDrawn
let drawnFeatures: DrawnFeature[] = []
if (shouldParse && rawDrawn && rawDrawn.length < 200_000) {
try {
const parsed = JSON.parse(rawDrawn)
if (Array.isArray(parsed)) drawnFeatures = parsed.slice(0, 200)
} catch {
// ignore
}
}Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: JSON.stringify(analysisResult), | ||
| content: JSON.stringify({ | ||
| ...analysisResult, | ||
| image: dataUrl | ||
| }), |
There was a problem hiding this comment.
You’re storing the full dataUrl (base64 image) into AI state (resolution_search_result). That can massively inflate persisted chat state, slow down UI state hydration, and potentially exceed storage limits depending on your saveChat implementation.
This is particularly risky because static map screenshots can be large, and every resolution search will permanently embed the image bytes into chat history.
Suggestion
Avoid persisting base64 data URLs in chat history. Prefer storing:
- a short-lived object storage URL (S3/GCS/R2) for the captured image, or
- a server-generated cache key referencing stored bytes, or
- omit the image from persisted state and rely on the already-rendered UI stream for the immediate session.
Minimal improvement if you can’t add storage yet: store only the original capture parameters (provider, center/zoom/bounds) and re-fetch on render.
Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
|
|
||
| return { | ||
| id, | ||
| component: ( |
There was a problem hiding this comment.
getUIStateFromAIState directly JSON.parses resolution_search_result without a try/catch. If older messages exist (or partial/corrupt content), this will throw during render and can break the entire chat UI.
You already handle backward compatibility in the user message case; this should get similar protection.
Suggestion
Wrap parsing with a guard and fail soft:
case 'resolution_search_result': {
let analysisResult: any
try {
analysisResult = JSON.parse(content as string)
} catch {
return { id, component: null }
}
const geoJson = analysisResult.geoJson as FeatureCollection
const image = analysisResult.image as string
// ...
}Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
| /* eslint-disable @next/next/no-img-element */ | ||
| 'use client' | ||
|
|
||
| import React from 'react' | ||
| import { Card, CardContent } from '@/components/ui/card' | ||
| import { | ||
| Dialog, | ||
| DialogContent, | ||
| DialogHeader, | ||
| DialogTitle, | ||
| DialogTrigger | ||
| } from '@/components/ui/dialog' | ||
| import { motion } from 'framer-motion' | ||
| import { cn } from '@/lib/utils' | ||
|
|
||
| interface ResolutionImageProps { | ||
| src: string | ||
| className?: string | ||
| alt?: string | ||
| } | ||
|
|
||
| export function ResolutionImage({ src, className, alt = 'Map Imagery' }: ResolutionImageProps) { | ||
| if (!src) return null | ||
|
|
||
| return ( | ||
| <div className={cn('mt-2 mb-4', className)}> | ||
| <Dialog> | ||
| <DialogTrigger asChild> | ||
| <motion.div | ||
| className="w-fit cursor-pointer relative glassmorphic overflow-hidden rounded-lg border bg-muted" | ||
| whileHover={{ scale: 1.02 }} | ||
| whileTap={{ scale: 0.98 }} | ||
| > | ||
| <Card className="border-none shadow-none bg-transparent"> | ||
| <CardContent className="p-2"> | ||
| <img | ||
| src={src} | ||
| alt={alt} | ||
| className="max-w-xs max-h-64 rounded-md object-contain" | ||
| /> | ||
| </CardContent> | ||
| </Card> | ||
| </motion.div> | ||
| </DialogTrigger> | ||
| <DialogContent className="sm:max-w-5xl max-h-[90vh] p-1 glassmorphic border-none"> | ||
| <DialogHeader className="sr-only"> | ||
| <DialogTitle>{alt}</DialogTitle> | ||
| </DialogHeader> | ||
| <div className="flex items-center justify-center h-full"> | ||
| <img | ||
| src={src} | ||
| alt={`${alt} Full`} | ||
| className="h-auto w-full object-contain max-h-[85vh] rounded-md" | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
ResolutionImage disables @next/next/no-img-element and uses raw <img> tags. If this app is on Next.js with the Image Optimization pipeline available, you’re opting out globally for this file without documenting why.
Also, because src may be a remote URL (e.g., mapUrl from the tool) or a data URL, you need to be explicit about security/perf tradeoffs (remote domains, referrer policy, loading behavior).
Suggestion
Prefer next/image when feasible, and only fall back to <img> for data: URLs.
One approach:
- If
src.startsWith('data:')use<img> - Else use
<Image>with configuredimages.remotePatterns
Additionally consider adding loading="lazy" to the thumbnail <img> and referrerPolicy="no-referrer" for remote map URLs.
Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
| const renderSuggestions = () => { | ||
| if (!suggestions) return null; | ||
| return ( | ||
| <div className="absolute inset-0 z-20 flex flex-col items-start p-4"> | ||
| <SuggestionsDropdown | ||
| suggestions={suggestions} | ||
| onSelect={query => { | ||
| setInput(query) | ||
| setSuggestions(null) | ||
| // Use a small timeout to ensure state update before submission | ||
| setTimeout(() => { | ||
| setIsSubmitting(true) | ||
| }, 0) | ||
| }} | ||
| onClose={() => setSuggestions(null)} | ||
| className="relative bottom-auto mb-0 w-full shadow-none border-none bg-transparent" | ||
| /> | ||
| </div> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
renderSuggestions() is recreated on every render and closes over state; that’s fine, but the new overlay uses absolute inset-0 with z-20 and the underlying container is now relative min-h-[100px]. On desktop, this risks overlaying only the messages area (not the whole chat column) depending on DOM structure.
More importantly: you’re using setTimeout(..., 0) to submit after setting input. That’s a brittle ordering hack; React state batching and concurrent rendering can still lead to edge cases.
Suggestion
Eliminate the setTimeout submission hack by submitting with the selected query directly (or by using a callback/ref that triggers submission after state commit):
- Option A: provide
ChatPanelRef.submitForm()and call it immediately after setting input (and close suggestions) - Option B: keep a
pendingSubmitQuerystate anduseEffectto submit when it changes
This makes follow-up clicks deterministic.
Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
| /** | ||
| * Helper to generate a Google Static Map URL | ||
| */ | ||
| function getGoogleStaticMapUrl(latitude: number, longitude: number): string { | ||
| const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || process.env.GOOGLE_MAPS_API_KEY; | ||
| if (!apiKey) return ''; | ||
| return `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&zoom=15&size=640x480&scale=2&markers=color:red%7C${latitude},${longitude}&key=${apiKey}`; | ||
| } |
There was a problem hiding this comment.
getGoogleStaticMapUrl reads NEXT_PUBLIC_GOOGLE_MAPS_API_KEY on the server side and falls back to GOOGLE_MAPS_API_KEY. This mixes client-exposed and server-only secrets. If the server has access to GOOGLE_MAPS_API_KEY, it should prefer that and only use the public key as a last resort.
Also, returning '' when the key is missing means downstream UI silently won’t show previews; that’s fine, but consider logging once (not per request) or surfacing a debug flag.
Suggestion
Prefer server-only env first:
const apiKey = process.env.GOOGLE_MAPS_API_KEY ?? process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEYOptionally, add a single warning log when mapProvider === 'google' and no key is configured.
Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/chat.tsx (1)
86-86:⚠️ Potential issue | 🔴 Critical
useMapDatais called outside ofMapDataProvidercontext.The
useMapData()hook is invoked at line 86, but theMapDataProviderwrappers are only added inside the mobile (line 130) and desktop (line 178) return blocks. This means the hook is called before the component tree is wrapped with the provider, which will cause a runtime error or return undefined context values.🐛 Proposed fix: Move MapDataProvider to wrap the entire component or move useMapData inside the conditional branches
Option 1 - Wrap the entire component with MapDataProvider at a higher level, or:
Option 2 - Create a wrapper component:
-export function Chat({ id }: ChatProps) { +function ChatContent({ id }: ChatProps) { const router = useRouter() // ... rest of component logic +} + +export function Chat({ id }: ChatProps) { + return ( + <MapDataProvider> + <ChatContent id={id} /> + </MapDataProvider> + ); }This ensures
useMapData()is always called within the provider context.
🤖 Fix all issues with AI agents
In `@app/actions.tsx`:
- Line 88: Rename the misspelled variable groupeId to groupId throughout the
file: change the declaration const groupeId = nanoid() to const groupId =
nanoid() and update every usage/reference (including the other occurrence noted
and any functions or JSX that consume it) to the new name to avoid runtime
undefined errors; ensure related imports/exports, state hooks, and props that
referenced groupeId are updated to groupId as well and run type checks/compile
to confirm no remaining references.
In `@components/resolution-image.tsx`:
- Around line 28-44: The DialogTrigger currently wraps a non-focusable
motion.div which prevents keyboard users from opening the dialog; update the
trigger to be keyboard accessible by replacing the motion.div with a focusable
element (e.g., use motion.button as the child of DialogTrigger or wrap a
<button> with motion props) or add role="button", tabIndex={0} and appropriate
onKeyDown handlers to the motion element; change the element used in the
DialogTrigger (look for DialogTrigger and the motion.div in ResolutionImage
component) so it becomes focusable and responds to Enter/Space to open the
dialog.
In `@lib/agents/tools/geospatial.tsx`:
- Around line 409-412: The current guard ignores valid 0 coordinates because it
uses truthy checks; update the condition in the block that sets mcpData.mapUrl
(checking mapProvider, mcpData.location.latitude, mcpData.location.longitude,
and mcpData.mapUrl) to explicitly test for null/undefined or use Number.isFinite
on mcpData.location.latitude and mcpData.location.longitude so
latitude/longitude of 0 are accepted, then call getGoogleStaticMapUrl as before.
- Around line 155-162: The getGoogleStaticMapUrl function currently falls back
to process.env.GOOGLE_MAPS_API_KEY which can expose a server-only secret in
client-rendered URLs; remove that fallback so the function only reads
process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, and if it's missing return an empty
string (i.e., update getGoogleStaticMapUrl to use solely
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY and do not reference GOOGLE_MAPS_API_KEY
anywhere).
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (10)
app/actions.tsxcomponents/chat-panel.tsxcomponents/chat.tsxcomponents/followup-panel.tsxcomponents/header-search-button.tsxcomponents/resolution-image.tsxcomponents/search-related.tsxcomponents/sidebar/chat-history-client.tsxlib/agents/researcher.tsxlib/agents/tools/geospatial.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-20T10:04:04.963Z
Learnt from: ngoiyaeric
Repo: QueueLab/QCX PR: 0
File: :0-0
Timestamp: 2026-01-20T10:04:04.963Z
Learning: The application has a comprehensive credits and payment UI system with: (1) PurchaseCreditsPopup that shows after user authentication with a 7-day cooldown, (2) History panel on the left side containing CreditsDisplay at the top and ChatHistoryClient below, (3) TentTree icon in header that opens UsageSidebar on the right side showing usage history and billing options. All these components are coordinated through RootLayout and only render for authenticated users.
Applied to files:
components/sidebar/chat-history-client.tsx
🧬 Code graph analysis (5)
components/header-search-button.tsx (1)
components/map/google-map.tsx (2)
features(38-52)df(39-46)
components/chat-panel.tsx (5)
components/map/mapbox-map.tsx (5)
feature(79-165)prevData(167-167)feature(301-303)map(67-168)map(240-308)components/map/google-map.tsx (2)
features(38-52)df(39-46)components/map/map-data-context.tsx (1)
MapData(15-32)components/map/map-query-handler.tsx (1)
prevData(39-48)components/map/google-geojson-layer.tsx (1)
feature(27-44)
app/actions.tsx (4)
lib/agents/resolution-search.tsx (2)
DrawnFeature(26-31)resolutionSearch(33-79)components/map/geojson-layer.tsx (1)
GeoJsonLayer(13-100)components/resolution-image.tsx (1)
ResolutionImage(22-60)components/map/map-query-handler.tsx (3)
MapQueryHandler(30-82)GeospatialToolOutput(18-23)prevData(39-48)
components/chat.tsx (5)
components/suggestions-dropdown.tsx (1)
SuggestionsDropdown(16-116)lib/utils/index.ts (1)
cn(12-14)components/empty-screen.tsx (1)
EmptyScreen(27-59)components/chat-messages.tsx (1)
ChatMessages(11-70)lib/db/schema.ts (1)
messages(26-37)
components/search-related.tsx (2)
lib/schema/related.tsx (1)
PartialRelated(13-13)components/copilot.tsx (1)
e(70-90)
🔇 Additional comments (17)
components/sidebar/chat-history-client.tsx (1)
124-136: No concerns with the layout-only change.Spacing-only adjustment, no behavioral impact detected.
components/search-related.tsx (1)
28-57: Solid switch to click-driven related queries.Clean handler flow with explicit FormData and nanoid IDs; no issues spotted.
components/header-search-button.tsx (1)
76-94: Changes look good.Static map scaling and drawnFeatures payload addition are consistent and safe.
components/chat-panel.tsx (1)
118-120: Drawn-features payload inclusion looks correct.No issues with the FormData augmentation.
components/followup-panel.tsx (1)
28-30: Drawn-features inclusion is consistent with the new flow.No concerns here.
lib/agents/researcher.tsx (3)
15-15: LGTM!Import of
DrawnFeaturetype is correctly added to support the new functionality.
20-27: LGTM!The system prompt enhancement is well-implemented:
- Cleanly extends the function signature with an optional parameter
- Conditionally includes drawn features only when present
- Template literal formatting is appropriate for multi-line prompt construction
88-89: LGTM!The
drawnFeaturesparameter is correctly:
- Added as an optional parameter at the end of the function signature
- Passed through to
getDefaultSystemPromptwhen building the system prompt- Only used when a custom dynamic prompt is not provided (line 102-105)
Also applies to: 105-105
components/chat.tsx (2)
106-125: LGTM!The
renderSuggestionshelper cleanly centralizes the suggestions dropdown logic:
- Properly guards against null suggestions
- Uses consistent timeout pattern for state synchronization before submission
- Correctly wires up
onSelectandonClosehandlers
151-167: LGTM!The layout refactoring is well-structured:
- Consistent pattern applied to both mobile and desktop layouts
- Blur effect on background content provides clear visual feedback
renderSuggestions()helper eliminates code duplicationAlso applies to: 193-208
app/actions.tsx (7)
44-50: LGTM!Robust error handling for JSON parsing of
drawnFeatures:
- Initializes to empty array before parsing
- Catches parse errors gracefully with console logging
- Falls back to empty array on failure
105-112: LGTM!GeoJsonLayer is correctly appended when
analysisResult.geoJsonexists, with proper null check and unique ID assignment.
148-151: LGTM!The image data URL is properly included in the resolution_search_result content, enabling the UI state reconstruction to display the image preview later via
getUIStateFromAIState.
179-184: LGTM!
ResolutionImageis correctly rendered in the initial response section to provide immediate visual feedback of the uploaded image alongside the streaming bot message.
410-411: LGTM!The
drawnFeaturesparameter is correctly passed to theresearcherfunction, completing the data flow from form submission through to the AI agent's system prompt.
702-718: LGTM!The
resolution_search_resultcase correctly:
- Parses the stored JSON content
- Extracts both
geoJsonandimagefields- Conditionally renders
ResolutionImageandGeoJsonLayercomponents
731-749: LGTM!The
MAP_QUERY_TRIGGERhandling is enhanced with:
- Proper extraction of
mapUrlandplace_nameusing optional chaining- Conditional rendering of
ResolutionImagewith descriptive alt textMapQueryHandlerremains as the primary functional component
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
|
|
||
| // Create a streamable value for the summary. | ||
| const summaryStream = createStreamableValue<string>(''); | ||
| const groupeId = nanoid(); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider renaming groupeId to groupId for clarity.
The variable name groupeId appears to be a typo or non-standard spelling. Using groupId would align with common English naming conventions and improve readability.
✏️ Suggested rename
- const groupeId = nanoid();
+ const groupId = nanoid();Apply the same change at line 203 and update all references.
🤖 Prompt for AI Agents
In `@app/actions.tsx` at line 88, Rename the misspelled variable groupeId to
groupId throughout the file: change the declaration const groupeId = nanoid() to
const groupId = nanoid() and update every usage/reference (including the other
occurrence noted and any functions or JSX that consume it) to the new name to
avoid runtime undefined errors; ensure related imports/exports, state hooks, and
props that referenced groupeId are updated to groupId as well and run type
checks/compile to confirm no remaining references.
| <DialogTrigger asChild> | ||
| <motion.div | ||
| className="w-fit cursor-pointer relative glassmorphic overflow-hidden rounded-lg border bg-muted" | ||
| whileHover={{ scale: 1.02 }} | ||
| whileTap={{ scale: 0.98 }} | ||
| > | ||
| <Card className="border-none shadow-none bg-transparent"> | ||
| <CardContent className="p-2"> | ||
| <img | ||
| src={src} | ||
| alt={alt} | ||
| className="max-w-xs max-h-64 rounded-md object-contain" | ||
| /> | ||
| </CardContent> | ||
| </Card> | ||
| </motion.div> | ||
| </DialogTrigger> |
There was a problem hiding this comment.
Make the dialog trigger keyboard-accessible.
DialogTrigger wraps a motion.div, which is not focusable by default, so keyboard users can’t open the dialog. Use a button (or add role/tabIndex + key handlers) to meet accessibility requirements.
♿ Suggested fix
- <DialogTrigger asChild>
- <motion.div
+ <DialogTrigger asChild>
+ <motion.button
+ type="button"
className="w-fit cursor-pointer relative glassmorphic overflow-hidden rounded-lg border bg-muted"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Card className="border-none shadow-none bg-transparent">
<CardContent className="p-2">
<img
src={src}
alt={alt}
className="max-w-xs max-h-64 rounded-md object-contain"
/>
</CardContent>
</Card>
- </motion.div>
+ </motion.button>
</DialogTrigger>📝 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.
| <DialogTrigger asChild> | |
| <motion.div | |
| className="w-fit cursor-pointer relative glassmorphic overflow-hidden rounded-lg border bg-muted" | |
| whileHover={{ scale: 1.02 }} | |
| whileTap={{ scale: 0.98 }} | |
| > | |
| <Card className="border-none shadow-none bg-transparent"> | |
| <CardContent className="p-2"> | |
| <img | |
| src={src} | |
| alt={alt} | |
| className="max-w-xs max-h-64 rounded-md object-contain" | |
| /> | |
| </CardContent> | |
| </Card> | |
| </motion.div> | |
| </DialogTrigger> | |
| <DialogTrigger asChild> | |
| <motion.button | |
| type="button" | |
| className="w-fit cursor-pointer relative glassmorphic overflow-hidden rounded-lg border bg-muted" | |
| whileHover={{ scale: 1.02 }} | |
| whileTap={{ scale: 0.98 }} | |
| > | |
| <Card className="border-none shadow-none bg-transparent"> | |
| <CardContent className="p-2"> | |
| <img | |
| src={src} | |
| alt={alt} | |
| className="max-w-xs max-h-64 rounded-md object-contain" | |
| /> | |
| </CardContent> | |
| </Card> | |
| </motion.button> | |
| </DialogTrigger> |
🤖 Prompt for AI Agents
In `@components/resolution-image.tsx` around lines 28 - 44, The DialogTrigger
currently wraps a non-focusable motion.div which prevents keyboard users from
opening the dialog; update the trigger to be keyboard accessible by replacing
the motion.div with a focusable element (e.g., use motion.button as the child of
DialogTrigger or wrap a <button> with motion props) or add role="button",
tabIndex={0} and appropriate onKeyDown handlers to the motion element; change
the element used in the DialogTrigger (look for DialogTrigger and the motion.div
in ResolutionImage component) so it becomes focusable and responds to
Enter/Space to open the dialog.
| /** | ||
| * Helper to generate a Google Static Map URL | ||
| */ | ||
| function getGoogleStaticMapUrl(latitude: number, longitude: number): string { | ||
| const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || process.env.GOOGLE_MAPS_API_KEY; | ||
| if (!apiKey) return ''; | ||
| return `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&zoom=15&size=640x480&scale=2&markers=color:red%7C${latitude},${longitude}&key=${apiKey}`; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find context around getGoogleStaticMapUrl usage
rg -B 3 -A 3 "getGoogleStaticMapUrl" lib/agents/tools/geospatial.tsxRepository: QueueLab/QCX
Length of output: 1054
🏁 Script executed:
# Check if mapUrl is sent in mcp_response and to client
rg "mapUrl" lib/agents/tools/geospatial.tsx -B 2 -A 2Repository: QueueLab/QCX
Length of output: 1421
🏁 Script executed:
# Search for mcp_response and how it's handled
rg "mcp_response" lib/agents/tools/geospatial.tsx -B 5 -A 5Repository: QueueLab/QCX
Length of output: 2103
🏁 Script executed:
# Check for any other GOOGLE_MAPS_API_KEY usage in codebase
rg "GOOGLE_MAPS_API_KEY" --type tsx --type tsRepository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Check environment variable patterns in codebase
rg "NEXT_PUBLIC_GOOGLE_MAPS" --type tsx --type ts -B 2 -A 2Repository: QueueLab/QCX
Length of output: 83
🌐 Web query:
Google Static Maps API key security best practices client-side exposure
💡 Result:
Static Maps API key security when the key is exposed client-side (e.g., in an <img> URL)
-
Assume the API key will be public (anyone can view/copy it from page source/network). Instead of trying to “hide” it, lock it down with restrictions. [1]
-
Apply both restriction types to the key used for Static Web APIs:
- Website restriction (HTTP referrers): allow only your domains.
- API restriction: allow only Maps Static API (and nothing else).
Google explicitly recommends applying both when the key is publicly exposed on a web page (like an<img>tag). [1]
-
Use a separate key per app/site (and per environment) (dev/staging/prod) to limit blast radius and simplify rotation. [1]
-
Use URL signing (digital signature) and keep the signing secret server-side
- Google strongly recommends using an API key + digital signature for Maps Static API. [2]
- Never sign in client-side JavaScript and never embed the URL signing secret in client code or your repo; instead generate signed URLs on your server (or via a secure proxy) and return/render the final signed URL. [1]
-
Limit/disable unsigned requests
- In Google Cloud Console quotas for Maps Static API, reduce “Unsigned requests” quota (commonly to 0) so only signed requests succeed. [2]
-
Operational hygiene
- Delete unused keys/services, monitor key usage, and rotate keys carefully (rotation can break deployed clients if not planned). [1]
- If you also have server-side Maps usage, split client vs server usage into separate projects/keys, and use appropriate restrictions (server keys typically use IP restrictions; web uses referrers). [1], [3]
Sources: Google Maps Platform API security best practices [1], Maps Static API digital signature guidance (incl. limiting unsigned requests) [2], Google Maps Platform FAQ on restrictions & Static Maps signatures [3].
🌐 Web query:
NEXT_PUBLIC environment variables security Next.js API key exposure
💡 Result:
- In Next.js, only env vars prefixed with
NEXT_PUBLIC_are exposed to the browser; Next.js inlines (hard-codes) their values into the client JavaScript bundle at build time (next build). Anyone can view them by inspecting the shipped JS. [1] - Therefore, never put secrets (API keys, tokens, DB creds) in
NEXT_PUBLIC_*—they should be treated as public. [1]
How to avoid API key exposure
- Keep secrets as server-only env vars (no
NEXT_PUBLIC_) and use them only in server contexts (Route Handlers/API routes, Server Actions, server components during dynamic rendering,getServerSideProps, etc.). [1] - If the browser needs data from a 3rd-party API that requires a secret, call that 3rd-party API from your server (Next.js route handler/API route) and return only the necessary result to the client. [1]
Common safe uses of NEXT_PUBLIC_*
- Non-secret configuration like analytics IDs, public base URLs, feature flags that are okay to reveal. [1]
Deployment note (Vercel)
- Vercel recognizes framework “public” prefixes like
NEXT_PUBLIC_for Next.js and makes them available during the build step accordingly—reinforcing that these are meant for client-visible values, not secrets. [2]
Rule of thumb: If leaking it would cost money or grant access, it must not be NEXT_PUBLIC_*. [1]
Sources:
[1] Next.js docs – Environment Variables (“Bundling Environment Variables for the Browser”, build-time inlining, NEXT_PUBLIC_) https://nextjs.org/docs/14/app/building-your-application/configuring/environment-variables
[2] Vercel docs – Framework environment variables (prefixes like NEXT_PUBLIC_) https://vercel.com/docs/environment-variables/framework-environment-variables
Remove the fallback to GOOGLE_MAPS_API_KEY to avoid exposing a server-only secret in client URLs.
The mcp_response containing mapUrl is returned to and rendered on the client. If GOOGLE_MAPS_API_KEY (a server-only variable) is used as the fallback, its value will be embedded in the URL and exposed to the browser. Use only NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, which is intended for client-side use and should be restricted to Maps Static API with HTTP referrer constraints in the Google Cloud Console.
🔒 Suggested fix
function getGoogleStaticMapUrl(latitude: number, longitude: number): string {
- const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || process.env.GOOGLE_MAPS_API_KEY;
+ const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY;
if (!apiKey) return '';
return `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&zoom=15&size=640x480&scale=2&markers=color:red%7C${latitude},${longitude}&key=${apiKey}`;
}📝 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.
| /** | |
| * Helper to generate a Google Static Map URL | |
| */ | |
| function getGoogleStaticMapUrl(latitude: number, longitude: number): string { | |
| const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || process.env.GOOGLE_MAPS_API_KEY; | |
| if (!apiKey) return ''; | |
| return `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&zoom=15&size=640x480&scale=2&markers=color:red%7C${latitude},${longitude}&key=${apiKey}`; | |
| } | |
| /** | |
| * Helper to generate a Google Static Map URL | |
| */ | |
| function getGoogleStaticMapUrl(latitude: number, longitude: number): string { | |
| const apiKey = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY; | |
| if (!apiKey) return ''; | |
| return `https://maps.googleapis.com/maps/api/staticmap?center=${latitude},${longitude}&zoom=15&size=640x480&scale=2&markers=color:red%7C${latitude},${longitude}&key=${apiKey}`; | |
| } |
🤖 Prompt for AI Agents
In `@lib/agents/tools/geospatial.tsx` around lines 155 - 162, The
getGoogleStaticMapUrl function currently falls back to
process.env.GOOGLE_MAPS_API_KEY which can expose a server-only secret in
client-rendered URLs; remove that fallback so the function only reads
process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY, and if it's missing return an empty
string (i.e., update getGoogleStaticMapUrl to use solely
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY and do not reference GOOGLE_MAPS_API_KEY
anywhere).
| // Enhance with Google Static Map URL if provider is google and we have coordinates | ||
| if (mapProvider === 'google' && mcpData.location.latitude && mcpData.location.longitude && !mcpData.mapUrl) { | ||
| mcpData.mapUrl = getGoogleStaticMapUrl(mcpData.location.latitude, mcpData.location.longitude); | ||
| } |
There was a problem hiding this comment.
Handle 0/0 coordinates correctly.
The truthy check skips valid coordinates at 0 latitude/longitude. Use explicit null/undefined or finite-number checks.
✅ Suggested fix
- if (mapProvider === 'google' && mcpData.location.latitude && mcpData.location.longitude && !mcpData.mapUrl) {
+ if (
+ mapProvider === 'google' &&
+ Number.isFinite(mcpData.location.latitude) &&
+ Number.isFinite(mcpData.location.longitude) &&
+ !mcpData.mapUrl
+ ) {
mcpData.mapUrl = getGoogleStaticMapUrl(mcpData.location.latitude, mcpData.location.longitude);
}📝 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.
| // Enhance with Google Static Map URL if provider is google and we have coordinates | |
| if (mapProvider === 'google' && mcpData.location.latitude && mcpData.location.longitude && !mcpData.mapUrl) { | |
| mcpData.mapUrl = getGoogleStaticMapUrl(mcpData.location.latitude, mcpData.location.longitude); | |
| } | |
| // Enhance with Google Static Map URL if provider is google and we have coordinates | |
| if ( | |
| mapProvider === 'google' && | |
| Number.isFinite(mcpData.location.latitude) && | |
| Number.isFinite(mcpData.location.longitude) && | |
| !mcpData.mapUrl | |
| ) { | |
| mcpData.mapUrl = getGoogleStaticMapUrl(mcpData.location.latitude, mcpData.location.longitude); | |
| } |
🤖 Prompt for AI Agents
In `@lib/agents/tools/geospatial.tsx` around lines 409 - 412, The current guard
ignores valid 0 coordinates because it uses truthy checks; update the condition
in the block that sets mcpData.mapUrl (checking mapProvider,
mcpData.location.latitude, mcpData.location.longitude, and mcpData.mapUrl) to
explicitly test for null/undefined or use Number.isFinite on
mcpData.location.latitude and mcpData.location.longitude so latitude/longitude
of 0 are accepted, then call getGoogleStaticMapUrl as before.
…archer agent - Restored side-by-side Mapbox and Google Satellite previews in ResolutionImage. - Updated HeaderSearchButton to capture both Mapbox and Google imagery. - Modified app/actions.tsx to process and display multi-image results. - Improved researcher agent's prompt to always include user-drawn features and measurements. - Switched to sonner for toast notifications in HeaderSearchButton for consistency. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
app/actions.tsx (1)
689-714:⚠️ Potential issue | 🟡 MinorWrap switch case declarations in block scope.
Variables declared with
const/letinside switch cases without block scope can leak to other cases, causing potential runtime issues. Biome flagged this at lines 690 and 703-705.🔧 Suggested fix
case 'assistant': + { const answer = createStreamableValue(content as string) answer.done(content as string) switch (type) { case 'response': return { id, component: ( <Section title="response"> <BotMessage content={answer.value} /> </Section> ) } - case 'related': - const relatedQueries = createStreamableValue<RelatedQueries>({ - items: [] - }) + case 'related': { + const relatedQueries = createStreamableValue<RelatedQueries>({ + items: [] + }) relatedQueries.done(JSON.parse(content as string)) return { id, component: ( <Section title="Related" separator={true}> <SearchRelated relatedQueries={relatedQueries.value} /> </Section> ) } + } case 'followup': ... } + } breakcomponents/header-search-button.tsx (1)
120-120: 🧹 Nitpick | 🔵 TrivialButton disabled when
!map, but Google-only capture may be possible.The button is disabled when
!map, but lines 74-87 can capture a Google preview usingmapData.cameraStateeven without a Mapbox map instance. Consider allowing the button whencameraState.centerexists:♻️ Alternative disabled condition
- disabled={isAnalyzing || !map || !actions} + disabled={isAnalyzing || (!map && !mapData.cameraState?.center) || !actions}Apply the same change to line 132 for the mobile button.
🤖 Fix all issues with AI agents
In `@app/actions.tsx`:
- Around line 191-196: The success path in processResolutionSearch currently
calls aiState.done() but doesn't call isGenerating.done(false) and
uiStream.done(), leaving streams incomplete; update the success branch (after
aiState.done()) to also call isGenerating.done(false) and uiStream.done() and
ensure summaryStream.done(...) is invoked consistently (mirror the catch block
cleanup) so all three streams (isGenerating, uiStream, summaryStream) are
completed on both success and error.
- Around line 171-174: The image field is being JSON.stringified twice causing
nested string encoding; update the payload construction in app/actions.tsx where
you build content (the object that spreads analysisResult) so that image is
stored as an object (image: { mapbox: mapboxDataUrl, google: googleDataUrl })
instead of JSON.stringify(...), and then update the read-side parsing logic that
inspects analysisResult.image (the code around the current try/catch at lines
~732-739) to expect and handle an object directly (remove the extra JSON.parse
attempt or fall back only if a string is encountered).
In `@update_actions_v2.py`:
- Around line 1-240: This migration script (update_actions_v2.py) is a one‑time
transformer that rewrites the blocks matching "if (action ===
'resolution_search')" and "case 'resolution_search_result': {" in
app/actions.tsx and should not live at the repo root; either delete the file or
move it to a dedicated archive (e.g., scripts/migrations/) and add a short
README noting it was used to inject/replace the "if (action ===
'resolution_search')" and "case 'resolution_search_result': {" blocks, the date,
and that it must not be re-run; if you keep it archived, also make it
non‑runnable (e.g., wrap main logic behind a clear guard or rename so it won't
be accidentally executed).
In `@update_actions.py`:
- Around line 1-374: Delete the one-time migration scripts update_actions.py and
update_actions_v2.py from the repository; these tools have already been applied
(the old "const file = formData?.get('file')" pattern is replaced by
"mapboxFile"/"googleFile") so keeping them causes confusion. Remove both files
and any references to them (CI jobs, npm scripts, docs) to avoid accidental
re-run or clutter.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
app/actions.tsxcomponents/header-search-button.tsxcomponents/resolution-image.tsxlib/agents/researcher.tsxupdate_actions.pyupdate_actions_v2.py
🧰 Additional context used
🧬 Code graph analysis (4)
update_actions.py (1)
lib/agents/resolution-search.tsx (2)
resolutionSearch(33-79)msg(64-64)
components/resolution-image.tsx (3)
lib/utils/index.ts (1)
cn(12-14)components/ui/dialog.tsx (5)
Dialog(112-112)DialogTrigger(116-116)DialogContent(117-117)DialogHeader(118-118)DialogTitle(120-120)components/ui/card.tsx (2)
Card(79-79)CardContent(79-79)
components/header-search-button.tsx (4)
components/map/google-map.tsx (2)
GoogleMapComponent(12-86)apiKey(20-29)components/map/mapbox-map.tsx (3)
map(337-358)prevData(347-356)prevData(167-167)components/map/map-3d.tsx (1)
prevData(35-45)components/map/map-query-handler.tsx (1)
prevData(39-48)
update_actions_v2.py (1)
lib/agents/resolution-search.tsx (2)
resolutionSearch(33-79)msg(64-64)
🪛 Biome (2.3.13)
app/actions.tsx
[error] 690-690: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 703-705: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
🪛 Ruff (0.14.14)
update_actions_v2.py
[warning] 3-3: Unnecessary mode argument
Remove mode argument
(UP015)
🔇 Additional comments (4)
components/resolution-image.tsx (1)
24-36: LGTM!The source fallback logic cleanly handles the three-prop scenario (
src,mapboxSrc,googleSrc) with sensible defaults, and the early return prevents rendering when no sources are available.lib/agents/researcher.tsx (1)
20-27: LGTM!The
drawnFeaturesintegration is well-structured: the default prompt embeds the context inline, while dynamic prompts get it appended viadrawnFeaturesContext. The identical wording ensures consistent behavior across both paths.Also applies to: 102-109
components/header-search-button.tsx (2)
59-71: LGTM!The dual-capture approach cleanly handles both Mapbox canvas capture and optional Google Static Map fetch. The zoom calculation fallback from
rangeis a reasonable approximation.
74-87: Good resilience: Google fetch failure doesn't abort the flow.The try/catch around the Google Static Map fetch ensures that if Google's API fails or is unavailable, the user can still proceed with just the Mapbox capture. This is a good UX pattern.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| content: JSON.stringify({ | ||
| ...analysisResult, | ||
| image: JSON.stringify({ mapbox: mapboxDataUrl, google: googleDataUrl }) | ||
| }), |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Double JSON.stringify creates nested string encoding.
The image data is stringified twice: once at line 173 and again as part of the outer JSON.stringify at line 171. While the read-side (lines 732-739) handles this with a try/catch fallback, consider storing the image object directly to simplify parsing:
♻️ Alternative approach
content: JSON.stringify({
...analysisResult,
- image: JSON.stringify({ mapbox: mapboxDataUrl, google: googleDataUrl })
+ image: { mapbox: mapboxDataUrl, google: googleDataUrl }
}),Then update the read side:
- const imageData = analysisResult.image as string;
+ const imageData = analysisResult.image;
let mapboxSrc = '';
let googleSrc = '';
if (imageData) {
- try {
- const parsed = JSON.parse(imageData);
- mapboxSrc = parsed.mapbox || '';
- googleSrc = parsed.google || '';
- } catch (e) {
- mapboxSrc = imageData;
- }
+ if (typeof imageData === 'string') {
+ // Legacy format fallback
+ mapboxSrc = imageData;
+ } else {
+ mapboxSrc = imageData.mapbox || '';
+ googleSrc = imageData.google || '';
+ }
}🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 171 - 174, The image field is being
JSON.stringified twice causing nested string encoding; update the payload
construction in app/actions.tsx where you build content (the object that spreads
analysisResult) so that image is stored as an object (image: { mapbox:
mapboxDataUrl, google: googleDataUrl }) instead of JSON.stringify(...), and then
update the read-side parsing logic that inspects analysisResult.image (the code
around the current try/catch at lines ~732-739) to expect and handle an object
directly (remove the extra JSON.parse attempt or fall back only if a string is
encountered).
| } catch (error) { | ||
| console.error('Error in resolution search:', error); | ||
| summaryStream.error(error); | ||
| } finally { | ||
| console.error('Failed to process resolution search:', error); | ||
| summaryStream.done('An error occurred during analysis.'); | ||
| isGenerating.done(false); | ||
| uiStream.done(); | ||
| } |
There was a problem hiding this comment.
Missing isGenerating.done(false) and uiStream.done() on success path.
In the processResolutionSearch function, the catch block calls isGenerating.done(false) and uiStream.done(), but the success path (after aiState.done()) does not. This could leave streams in an incomplete state.
🐛 Proposed fix
}
]
})
+ isGenerating.done(false)
+ uiStream.done()
} catch (error) {
console.error('Failed to process resolution search:', error);
summaryStream.done('An error occurred during analysis.');
isGenerating.done(false);
uiStream.done();
}🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 191 - 196, The success path in
processResolutionSearch currently calls aiState.done() but doesn't call
isGenerating.done(false) and uiStream.done(), leaving streams incomplete; update
the success branch (after aiState.done()) to also call isGenerating.done(false)
and uiStream.done() and ensure summaryStream.done(...) is invoked consistently
(mirror the catch block cleanup) so all three streams (isGenerating, uiStream,
summaryStream) are completed on both success and error.
update_actions_v2.py
Outdated
| import sys | ||
|
|
||
| with open('app/actions.tsx', 'r') as f: | ||
| lines = f.readlines() | ||
|
|
||
| start_idx = -1 | ||
| end_idx = -1 | ||
|
|
||
| for i, line in enumerate(lines): | ||
| if "if (action === 'resolution_search') {" in line: | ||
| start_idx = i | ||
| # Find the matching closing brace | ||
| brace_count = 0 | ||
| for j in range(i, len(lines)): | ||
| brace_count += lines[j].count('{') | ||
| brace_count -= lines[j].count('}') | ||
| if brace_count == 0: | ||
| end_idx = j | ||
| break | ||
| break | ||
|
|
||
| if start_idx != -1 and end_idx != -1: | ||
| print(f"Replacing lines {start_idx+1} to {end_idx+1}") | ||
| new_block = """ if (action === 'resolution_search') { | ||
| const mapboxFile = formData?.get('mapboxFile') as File; | ||
| const googleFile = formData?.get('googleFile') as File; | ||
| const legacyFile = formData?.get('file') as File; | ||
| const timezone = (formData?.get('timezone') as string) || 'UTC'; | ||
|
|
||
| let mapboxDataUrl = ''; | ||
| let googleDataUrl = ''; | ||
|
|
||
| if (mapboxFile) { | ||
| const buffer = await mapboxFile.arrayBuffer(); | ||
| mapboxDataUrl = `data:${mapboxFile.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
| } | ||
| if (googleFile) { | ||
| const buffer = await googleFile.arrayBuffer(); | ||
| googleDataUrl = `data:${googleFile.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
| } | ||
|
|
||
| // Fallback if only 'file' was provided (backward compatibility) | ||
| if (!mapboxDataUrl && !googleDataUrl && legacyFile) { | ||
| const buffer = await legacyFile.arrayBuffer(); | ||
| mapboxDataUrl = `data:${legacyFile.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
| } | ||
|
|
||
| if (!mapboxDataUrl && !googleDataUrl) { | ||
| throw new Error('No files provided for resolution search.'); | ||
| } | ||
|
|
||
| const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( | ||
| message => | ||
| message.role !== 'tool' && | ||
| message.type !== 'followup' && | ||
| message.type !== 'related' && | ||
| message.type !== 'end' && | ||
| message.type !== 'resolution_search_result' | ||
| ); | ||
|
|
||
| const userInput = 'Analyze this map view.'; | ||
| const contentParts: any[] = [{ type: 'text', text: userInput }] | ||
|
|
||
| if (mapboxDataUrl) { | ||
| contentParts.push({ type: 'image', image: mapboxDataUrl, mimeType: 'image/png' }) | ||
| } | ||
| if (googleDataUrl) { | ||
| contentParts.push({ type: 'image', image: googleDataUrl, mimeType: 'image/png' }) | ||
| } | ||
|
|
||
| const content = contentParts as any | ||
|
|
||
| aiState.update({ | ||
| ...aiState.get(), | ||
| messages: [ | ||
| ...aiState.get().messages, | ||
| { id: nanoid(), role: 'user', content, type: 'input' } | ||
| ] | ||
| }); | ||
| messages.push({ role: 'user', content }); | ||
|
|
||
| const summaryStream = createStreamableValue<string>(''); | ||
| const groupeId = nanoid(); | ||
|
|
||
| async function processResolutionSearch() { | ||
| try { | ||
| const streamResult = await resolutionSearch(messages, timezone, drawnFeatures); | ||
|
|
||
| let fullSummary = ''; | ||
| for await (const partialObject of streamResult.partialObjectStream) { | ||
| if (partialObject.summary) { | ||
| fullSummary = partialObject.summary; | ||
| summaryStream.update(fullSummary); | ||
| } | ||
| } | ||
|
|
||
| const analysisResult = await streamResult.object; | ||
| summaryStream.done(analysisResult.summary || 'Analysis complete.'); | ||
|
|
||
| if (analysisResult.geoJson) { | ||
| uiStream.append( | ||
| <GeoJsonLayer | ||
| id={groupeId} | ||
| data={analysisResult.geoJson as FeatureCollection} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); | ||
|
|
||
| const sanitizedMessages: CoreMessage[] = messages.map(m => { | ||
| if (Array.isArray(m.content)) { | ||
| return { | ||
| ...m, | ||
| content: m.content.filter(part => part.type !== 'image') | ||
| } as CoreMessage | ||
| } | ||
| return m | ||
| }) | ||
|
|
||
| const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); | ||
| uiStream.append( | ||
| <Section title="Follow-up"> | ||
| <FollowupPanel /> | ||
| </Section> | ||
| ); | ||
|
|
||
| await new Promise(resolve => setTimeout(resolve, 500)); | ||
|
|
||
| aiState.done({ | ||
| ...aiState.get(), | ||
| messages: [ | ||
| ...aiState.get().messages, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: analysisResult.summary || 'Analysis complete.', | ||
| type: 'response' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: JSON.stringify({ | ||
| ...analysisResult, | ||
| image: JSON.stringify({ mapbox: mapboxDataUrl, google: googleDataUrl }) | ||
| }), | ||
| type: 'resolution_search_result' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: JSON.stringify(relatedQueries), | ||
| type: 'related' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: 'followup', | ||
| type: 'followup' | ||
| } | ||
| ] | ||
| }) | ||
| } catch (error) { | ||
| console.error('Failed to process resolution search:', error); | ||
| summaryStream.done('An error occurred during analysis.'); | ||
| isGenerating.done(false); | ||
| uiStream.done(); | ||
| } | ||
| } | ||
|
|
||
| processResolutionSearch(); | ||
|
|
||
| uiStream.update( | ||
| <Section title="response"> | ||
| <ResolutionImage mapboxSrc={mapboxDataUrl} googleSrc={googleDataUrl} /> | ||
| <BotMessage content={summaryStream.value} /> | ||
| </Section> | ||
| ); | ||
|
|
||
| return { | ||
| id: nanoid(), | ||
| isGenerating: isGenerating.value, | ||
| component: uiStream.value, | ||
| isCollapsed: isCollapsed.value | ||
| }; | ||
| } | ||
| """ | ||
| lines[start_idx:end_idx+1] = [new_block] | ||
|
|
||
| # Replace getUIStateFromAIState block | ||
| for i, line in enumerate(lines): | ||
| if "case 'resolution_search_result': {" in line: | ||
| start_idx = i | ||
| brace_count = 0 | ||
| for j in range(i, len(lines)): | ||
| brace_count += lines[j].count('{') | ||
| brace_count -= lines[j].count('}') | ||
| if brace_count == 0: | ||
| end_idx = j | ||
| break | ||
|
|
||
| print(f"Replacing UI block at line {start_idx+1} to {end_idx+1}") | ||
| new_ui = """ case 'resolution_search_result': { | ||
| const analysisResult = JSON.parse(content as string); | ||
| const geoJson = analysisResult.geoJson as FeatureCollection; | ||
| const imageData = analysisResult.image as string; | ||
| let mapboxSrc = ''; | ||
| let googleSrc = ''; | ||
|
|
||
| if (imageData) { | ||
| try { | ||
| const parsed = JSON.parse(imageData); | ||
| mapboxSrc = parsed.mapbox || ''; | ||
| googleSrc = parsed.google || ''; | ||
| } catch (e) { | ||
| // Fallback for older image format which was just a single string | ||
| mapboxSrc = imageData; | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| id, | ||
| component: ( | ||
| <> | ||
| {(mapboxSrc || googleSrc) && ( | ||
| <ResolutionImage mapboxSrc={mapboxSrc} googleSrc={googleSrc} /> | ||
| )} | ||
| {geoJson && ( | ||
| <GeoJsonLayer id={id} data={geoJson} /> | ||
| )} | ||
| </> | ||
| ) | ||
| } | ||
| } | ||
| """ | ||
| lines[start_idx:end_idx+1] = [new_ui] | ||
| break | ||
|
|
||
| with open('app/actions.tsx', 'w') as f: | ||
| f.writelines(lines) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if the patch script's target code already exists in app/actions.tsx
echo "=== Checking for mapboxFile pattern (from resolution_search block) ==="
rg -n "const mapboxFile = formData" app/actions.tsx 2>/dev/null | head -5 || echo "Not found"
echo ""
echo "=== Checking for imageData pattern (from resolution_search_result case) ==="
rg -n "const imageData = analysisResult.image" app/actions.tsx 2>/dev/null | head -5 || echo "Not found"
echo ""
echo "=== Checking if update_actions_v2.py exists ==="
ls -la update_actions_v2.py 2>/dev/null || echo "Script not found"
echo ""
echo "=== Checking if app/actions.tsx exists ==="
ls -la app/actions.tsx 2>/dev/null || echo "File not found"Repository: QueueLab/QCX
Length of output: 563
Remove or archive this patch script — the transformation has already been applied.
Both code blocks targeted by this script already exist in app/actions.tsx (lines 53 and 727). Since this is a one-time migration utility, it should be removed from the repository or moved to a scripts/migrations/ directory with clear documentation about when and why it was used. Leaving it at the root risks accidental re-execution.
🧰 Tools
🪛 Ruff (0.14.14)
[warning] 3-3: Unnecessary mode argument
Remove mode argument
(UP015)
🤖 Prompt for AI Agents
In `@update_actions_v2.py` around lines 1 - 240, This migration script
(update_actions_v2.py) is a one‑time transformer that rewrites the blocks
matching "if (action === 'resolution_search')" and "case
'resolution_search_result': {" in app/actions.tsx and should not live at the
repo root; either delete the file or move it to a dedicated archive (e.g.,
scripts/migrations/) and add a short README noting it was used to inject/replace
the "if (action === 'resolution_search')" and "case 'resolution_search_result':
{" blocks, the date, and that it must not be re-run; if you keep it archived,
also make it non‑runnable (e.g., wrap main logic behind a clear guard or rename
so it won't be accidentally executed).
update_actions.py
Outdated
| import sys | ||
|
|
||
| content = open('app/actions.tsx').read() | ||
|
|
||
| # Replace resolution_search block | ||
| search_block = """ if (action === 'resolution_search') { | ||
| const file = formData?.get('file') as File; | ||
| const timezone = (formData?.get('timezone') as string) || 'UTC'; | ||
|
|
||
| if (!file) { | ||
| throw new Error('No file provided for resolution search.'); | ||
| } | ||
|
|
||
| const buffer = await file.arrayBuffer(); | ||
| const dataUrl = `data:${file.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
|
|
||
| const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( | ||
| message => | ||
| message.role !== 'tool' && | ||
| message.type !== 'followup' && | ||
| message.type !== 'related' && | ||
| message.type !== 'end' && | ||
| message.type !== 'resolution_search_result' | ||
| ); | ||
|
|
||
| const userInput = 'Analyze this map view.'; | ||
| const content: CoreMessage['content'] = [ | ||
| { type: 'text', text: userInput }, | ||
| { type: 'image', image: dataUrl, mimeType: file.type } | ||
| ]; | ||
|
|
||
| aiState.update({ | ||
| ...aiState.get(), | ||
| messages: [ | ||
| ...aiState.get().messages, | ||
| { id: nanoid(), role: 'user', content, type: 'input' } | ||
| ] | ||
| }); | ||
| messages.push({ role: 'user', content }); | ||
|
|
||
| const summaryStream = createStreamableValue<string>(''); | ||
| const groupeId = nanoid(); | ||
|
|
||
| async function processResolutionSearch() { | ||
| try { | ||
| const streamResult = await resolutionSearch(messages, timezone, drawnFeatures); | ||
|
|
||
| let fullSummary = ''; | ||
| for await (const partialObject of streamResult.partialObjectStream) { | ||
| if (partialObject.summary) { | ||
| fullSummary = partialObject.summary; | ||
| summaryStream.update(fullSummary); | ||
| } | ||
| } | ||
|
|
||
| const analysisResult = await streamResult.object; | ||
| summaryStream.done(analysisResult.summary || 'Analysis complete.'); | ||
|
|
||
| if (analysisResult.geoJson) { | ||
| uiStream.append( | ||
| <GeoJsonLayer | ||
| id={groupeId} | ||
| data={analysisResult.geoJson as FeatureCollection} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); | ||
|
|
||
| const sanitizedMessages: CoreMessage[] = messages.map(m => { | ||
| if (Array.isArray(m.content)) { | ||
| return { | ||
| ...m, | ||
| content: m.content.filter(part => part.type !== 'image') | ||
| } as CoreMessage | ||
| } | ||
| return m | ||
| }) | ||
|
|
||
| const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); | ||
| uiStream.append( | ||
| <Section title="Follow-up"> | ||
| <FollowupPanel /> | ||
| </Section> | ||
| ); | ||
|
|
||
| await new Promise(resolve => setTimeout(resolve, 500)); | ||
|
|
||
| aiState.done({ | ||
| ...aiState.get(), | ||
| messages: [ | ||
| ...aiState.get().messages, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: analysisResult.summary || 'Analysis complete.', | ||
| type: 'response' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: JSON.stringify({ | ||
| ...analysisResult, | ||
| image: dataUrl | ||
| }), | ||
| type: 'resolution_search_result' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: JSON.stringify(relatedQueries), | ||
| type: 'related' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: 'followup', | ||
| type: 'followup' | ||
| } | ||
| ] | ||
| }) | ||
| } catch (error) { | ||
| console.error('Failed to process resolution search:', error); | ||
| summaryStream.done('An error occurred during analysis.'); | ||
| isGenerating.done(false); | ||
| uiStream.done(); | ||
| } | ||
| } | ||
|
|
||
| processResolutionSearch(); | ||
|
|
||
| uiStream.update( | ||
| <Section title="response"> | ||
| <ResolutionImage src={dataUrl} /> | ||
| <BotMessage content={summaryStream.value} /> | ||
| </Section> | ||
| ); | ||
|
|
||
| return { | ||
| id: nanoid(), | ||
| isGenerating: isGenerating.value, | ||
| component: uiStream.value, | ||
| isCollapsed: isCollapsed.value | ||
| } | ||
| }""" | ||
|
|
||
| replace_block = """ if (action === 'resolution_search') { | ||
| const mapboxFile = formData?.get('mapboxFile') as File; | ||
| const googleFile = formData?.get('googleFile') as File; | ||
| const legacyFile = formData?.get('file') as File; | ||
| const timezone = (formData?.get('timezone') as string) || 'UTC'; | ||
|
|
||
| let mapboxDataUrl = ''; | ||
| let googleDataUrl = ''; | ||
|
|
||
| if (mapboxFile) { | ||
| const buffer = await mapboxFile.arrayBuffer(); | ||
| mapboxDataUrl = `data:${mapboxFile.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
| } | ||
| if (googleFile) { | ||
| const buffer = await googleFile.arrayBuffer(); | ||
| googleDataUrl = `data:${googleFile.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
| } | ||
|
|
||
| // Fallback if only 'file' was provided (backward compatibility) | ||
| if (!mapboxDataUrl && !googleDataUrl && legacyFile) { | ||
| const buffer = await legacyFile.arrayBuffer(); | ||
| mapboxDataUrl = `data:${legacyFile.type};base64,${Buffer.from(buffer).toString('base64')}`; | ||
| } | ||
|
|
||
| if (!mapboxDataUrl && !googleDataUrl) { | ||
| throw new Error('No files provided for resolution search.'); | ||
| } | ||
|
|
||
| const messages: CoreMessage[] = [...(aiState.get().messages as any[])].filter( | ||
| message => | ||
| message.role !== 'tool' && | ||
| message.type !== 'followup' && | ||
| message.type !== 'related' && | ||
| message.type !== 'end' && | ||
| message.type !== 'resolution_search_result' | ||
| ); | ||
|
|
||
| const userInput = 'Analyze this map view.'; | ||
| const contentParts: any[] = [{ type: 'text', text: userInput }] | ||
|
|
||
| if (mapboxDataUrl) { | ||
| contentParts.push({ type: 'image', image: mapboxDataUrl, mimeType: 'image/png' }) | ||
| } | ||
| if (googleDataUrl) { | ||
| contentParts.push({ type: 'image', image: googleDataUrl, mimeType: 'image/png' }) | ||
| } | ||
|
|
||
| const content = contentParts as any | ||
|
|
||
| aiState.update({ | ||
| ...aiState.get(), | ||
| messages: [ | ||
| ...aiState.get().messages, | ||
| { id: nanoid(), role: 'user', content, type: 'input' } | ||
| ] | ||
| }); | ||
| messages.push({ role: 'user', content }); | ||
|
|
||
| const summaryStream = createStreamableValue<string>(''); | ||
| const groupeId = nanoid(); | ||
|
|
||
| async function processResolutionSearch() { | ||
| try { | ||
| const streamResult = await resolutionSearch(messages, timezone, drawnFeatures); | ||
|
|
||
| let fullSummary = ''; | ||
| for await (const partialObject of streamResult.partialObjectStream) { | ||
| if (partialObject.summary) { | ||
| fullSummary = partialObject.summary; | ||
| summaryStream.update(fullSummary); | ||
| } | ||
| } | ||
|
|
||
| const analysisResult = await streamResult.object; | ||
| summaryStream.done(analysisResult.summary || 'Analysis complete.'); | ||
|
|
||
| if (analysisResult.geoJson) { | ||
| uiStream.append( | ||
| <GeoJsonLayer | ||
| id={groupeId} | ||
| data={analysisResult.geoJson as FeatureCollection} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' }); | ||
|
|
||
| const sanitizedMessages: CoreMessage[] = messages.map(m => { | ||
| if (Array.isArray(m.content)) { | ||
| return { | ||
| ...m, | ||
| content: m.content.filter(part => part.type !== 'image') | ||
| } as CoreMessage | ||
| } | ||
| return m | ||
| }) | ||
|
|
||
| const relatedQueries = await querySuggestor(uiStream, sanitizedMessages); | ||
| uiStream.append( | ||
| <Section title="Follow-up"> | ||
| <FollowupPanel /> | ||
| </Section> | ||
| ); | ||
|
|
||
| await new Promise(resolve => setTimeout(resolve, 500)); | ||
|
|
||
| aiState.done({ | ||
| ...aiState.get(), | ||
| messages: [ | ||
| ...aiState.get().messages, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: analysisResult.summary || 'Analysis complete.', | ||
| type: 'response' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: JSON.stringify({ | ||
| ...analysisResult, | ||
| image: JSON.stringify({ mapbox: mapboxDataUrl, google: googleDataUrl }) | ||
| }), | ||
| type: 'resolution_search_result' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: JSON.stringify(relatedQueries), | ||
| type: 'related' | ||
| }, | ||
| { | ||
| id: groupeId, | ||
| role: 'assistant', | ||
| content: 'followup', | ||
| type: 'followup' | ||
| } | ||
| ] | ||
| }) | ||
| } catch (error) { | ||
| console.error('Failed to process resolution search:', error); | ||
| summaryStream.done('An error occurred during analysis.'); | ||
| isGenerating.done(false); | ||
| uiStream.done(); | ||
| } | ||
| } | ||
|
|
||
| processResolutionSearch(); | ||
|
|
||
| uiStream.update( | ||
| <Section title="response"> | ||
| <ResolutionImage mapboxSrc={mapboxDataUrl} googleSrc={googleDataUrl} /> | ||
| <BotMessage content={summaryStream.value} /> | ||
| </Section> | ||
| ); | ||
|
|
||
| return { | ||
| id: nanoid(), | ||
| isGenerating: isGenerating.value, | ||
| component: uiStream.value, | ||
| isCollapsed: isCollapsed.value | ||
| } | ||
| }""" | ||
|
|
||
| if search_block in content: | ||
| content = content.replace(search_block, replace_block) | ||
| else: | ||
| print("Warning: resolution_search block not found!") | ||
|
|
||
| # Replace resolution_search_result in getUIStateFromAIState | ||
| search_ui = """ case 'resolution_search_result': { | ||
| const analysisResult = JSON.parse(content as string); | ||
| const geoJson = analysisResult.geoJson as FeatureCollection; | ||
| const image = analysisResult.image as string; | ||
|
|
||
| return { | ||
| id, | ||
| component: ( | ||
| <> | ||
| {image && <ResolutionImage src={image} />} | ||
| {geoJson && ( | ||
| <GeoJsonLayer id={id} data={geoJson} /> | ||
| )} | ||
| </> | ||
| ) | ||
| } | ||
| }""" | ||
|
|
||
| replace_ui = """ case 'resolution_search_result': { | ||
| const analysisResult = JSON.parse(content as string); | ||
| const geoJson = analysisResult.geoJson as FeatureCollection; | ||
| const imageData = analysisResult.image as string; | ||
| let mapboxSrc = ''; | ||
| let googleSrc = ''; | ||
|
|
||
| if (imageData) { | ||
| try { | ||
| const parsed = JSON.parse(imageData); | ||
| mapboxSrc = parsed.mapbox || ''; | ||
| googleSrc = parsed.google || ''; | ||
| } catch (e) { | ||
| // Fallback for older image format which was just a single string | ||
| mapboxSrc = imageData; | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| id, | ||
| component: ( | ||
| <> | ||
| {(mapboxSrc || googleSrc) && ( | ||
| <ResolutionImage mapboxSrc={mapboxSrc} googleSrc={googleSrc} /> | ||
| )} | ||
| {geoJson && ( | ||
| <GeoJsonLayer id={id} data={geoJson} /> | ||
| )} | ||
| </> | ||
| ) | ||
| } | ||
| }""" | ||
|
|
||
| if search_ui in content: | ||
| content = content.replace(search_ui, replace_ui) | ||
| else: | ||
| print("Warning: resolution_search_result block not found!") | ||
|
|
||
| with open('app/actions.tsx', 'w') as f: | ||
| f.write(content) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "=== Looking for update_actions scripts ==="
fd -t f "update_actions" --extension py
echo ""
echo "=== Checking app/actions.tsx for transformation markers ==="
if [ -f "app/actions.tsx" ]; then
echo "File exists. Checking for new transformation evidence..."
rg "mapboxFile|googleFile" app/actions.tsx | head -5
echo "---"
rg "mapboxSrc|googleSrc" app/actions.tsx | head -5
else
echo "app/actions.tsx does not exist"
fi
echo ""
echo "=== Listing all files in repository root and key directories ==="
ls -la | head -20Repository: QueueLab/QCX
Length of output: 2145
🏁 Script executed:
#!/bin/bash
echo "=== Comparing update_actions.py and update_actions_v2.py ==="
echo ""
echo "File sizes:"
wc -l update_actions.py update_actions_v2.py
echo ""
echo "=== Git history of these files ==="
git log --oneline update_actions.py update_actions_v2.py 2>/dev/null | head -10
echo ""
echo "=== Checking for differences between the two scripts ==="
diff -u update_actions.py update_actions_v2.py | head -100Repository: QueueLab/QCX
Length of output: 3592
🏁 Script executed:
#!/bin/bash
echo "=== Checking if OLD code patterns still exist in app/actions.tsx ==="
echo "Looking for single 'file' parameter (old pattern):"
rg "const file = formData\?\.get\('file'\)" app/actions.tsx
echo "Exit code: $?"
echo ""
echo "=== Checking if NEW code patterns are present (new migration) ==="
echo "Looking for mapboxFile and googleFile:"
rg "const (mapboxFile|googleFile)" app/actions.tsx | wc -lRepository: QueueLab/QCX
Length of output: 296
Remove stale migration scripts to avoid confusion.
Both update_actions.py and update_actions_v2.py are one-time migration tools that have already been applied to app/actions.tsx (verified: old const file = formData?.get('file') pattern is gone, new mapboxFile/googleFile patterns are present). They serve no further purpose and should be deleted to keep the repository clean.
🤖 Prompt for AI Agents
In `@update_actions.py` around lines 1 - 374, Delete the one-time migration
scripts update_actions.py and update_actions_v2.py from the repository; these
tools have already been applied (the old "const file = formData?.get('file')"
pattern is replaced by "mapboxFile"/"googleFile") so keeping them causes
confusion. Remove both files and any references to them (CI jobs, npm scripts,
docs) to avoid accidental re-run or clutter.
- Synchronized with latest main branch (commit 649e109). - Restored multi-image (Mapbox and Google Satellite) preview support in ResolutionImage and HeaderSearchButton. - Updated app/actions.tsx to process dual imagery while preserving recent performance and stability fixes (e.g., sanitized history, initialized summary stream). - Improved researcher agent's prompt to include full drawing context (type, measurement, and geometry). - Switched to sonner for toast notifications in HeaderSearchButton. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
|
@coderabbitai how both images be returned like this |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 3788 🏁 Script executed: Length of output: 4997 🏁 Script executed: Length of output: 1278
The Flow1. Capture Phase (header-search-button.tsx, lines 60-78)Both images are captured separately: let mapboxBlob: Blob | null = null;
let googleBlob: Blob | null = null;
// Capture Mapbox canvas
if (map) {
const canvas = map.getCanvas();
mapboxBlob = await new Promise<Blob | null>(resolve => {
canvas.toBlob(resolve, 'image/png');
});
}
// Fetch Google Static Map via API
if (center) {
const staticMapUrl = `https://maps.googleapis.com/maps/api/staticmap?...`;
const response = await fetch(staticMapUrl);
googleBlob = await response.blob();
}2. Upload Phase (header-search-button.tsx, lines 85-87)Both files are sent separately: if (mapboxBlob) formData.append('mapboxFile', mapboxBlob, 'mapbox_capture.png')
if (googleBlob) formData.append('googleFile', googleBlob, 'google_capture.png')3. Processing Phase (actions.tsx)Both files are converted to data URLs: let mapboxDataUrl = '';
let googleDataUrl = '';
if (mapboxFile) {
const buffer = await mapboxFile.arrayBuffer();
mapboxDataUrl = `data:${mapboxFile.type};base64,${Buffer.from(buffer).toString('base64')}`;
}
if (googleFile) {
const buffer = await googleFile.arrayBuffer();
googleDataUrl = `data:${googleFile.type};base64,${Buffer.from(buffer).toString('base64')}`;
}4. Rendering Phase (resolution-image.tsx)The const hasBoth = mSrc && gSrc // Check if both sources exist
// Thumbnail view uses 2-column grid when both exist
<CardContent className={cn("p-2 grid gap-2", hasBoth ? "grid-cols-2" : "grid-cols-1")}>
{mSrc && (
<div className="space-y-1">
<img src={mSrc} alt={`${alt} Mapbox`} ... />
<p>Mapbox</p>
</div>
)}
{gSrc && (
<div className="space-y-1">
<img src={gSrc} alt={`${alt} Google`} ... />
<p>Google Satellite</p>
</div>
)}
</CardContent>Key Design PatternThe component uses conditional grid layout:
This design maintains backward compatibility while supporting the new dual-image feature! 🎨 |

User description
This pull request merges the enhancements from commit f89e3f7 into the main branch. These changes improve the resolution search capability by integrating user-drawn map features into the AI's context and providing better visual feedback via the new ResolutionImage component. It also fixes non-clickable follow-up queries and refactors the suggestions UI for a more fluid user experience.
PR created automatically by Jules for task 8671537456349162349 started by @ngoiyaeric
PR Type
Enhancement
Description
Integrate drawn map features into AI context for spatial awareness
Add ResolutionImage component for visual feedback in chat
Refactor suggestions UI for better interactivity and accessibility
Include map imagery in resolution search results and tool outputs
Pass drawn features to researcher agent for improved analysis
Diagram Walkthrough
File Walkthrough
9 files
Integrate drawn features and add ResolutionImage displayInclude drawn features in form submissionRefactor suggestions rendering for better UXAdd drawn features to followup query submissionInclude drawn features in resolution search requestNew component for displaying map imagery with modalRefactor to use click handlers instead of form submissionAdd drawn features to system prompt contextGenerate Google Static Map URLs for query results1 files
Minor whitespace formatting cleanupSummary by CodeRabbit
New Features
Improvements