Skip to content

Replace glassmorphic with Tailwind utilities#488

Open
ngoiyaeric wants to merge 4 commits intomainfrom
replace-glassmorphic-with-tailwind-9822029702985678452
Open

Replace glassmorphic with Tailwind utilities#488
ngoiyaeric wants to merge 4 commits intomainfrom
replace-glassmorphic-with-tailwind-9822029702985678452

Conversation

@ngoiyaeric
Copy link
Collaborator

@ngoiyaeric ngoiyaeric commented Feb 4, 2026

User description

This change replaces the external glassmorphic dependency and its corresponding CSS class with native Tailwind CSS utilities. This improves maintainability and reduces bundle size while maintaining the desired visual effect.

Key changes:

  • In components/search-results-image.tsx, the glassmorphic class on the image thumbnails and the DialogContent was replaced with a combination of backdrop-blur-md, bg-background/30 (or bg-background/80), and border-border/40.
  • The Card component within the thumbnails was made transparent to allow the backdrop blur from the wrapper to be visible.
  • The glassmorphic package was removed from package.json and the import was deleted.
  • bun install was run to update the lockfile.

PR created automatically by Jules for task 9822029702985678452 started by @ngoiyaeric


PR Type

Enhancement


Description

  • Removed glassmorphic npm package dependency

  • Replaced .glassmorphic class with Tailwind utilities

  • Applied backdrop-blur-md, theme-aware backgrounds, borders

  • Made nested Card components transparent for visual consistency


Diagram Walkthrough

flowchart LR
  A["glassmorphic package"] -->|removed| B["package.json"]
  C["glassmorphic CSS import"] -->|deleted| D["search-results-image.tsx"]
  E["Tailwind utilities"] -->|added| F["backdrop-blur-md + bg-background + border"]
  F -->|applied to| G["image thumbnails & dialog"]
  H["Card component"] -->|made transparent| I["bg-transparent border-none shadow-none"]
Loading

File Walkthrough

Relevant files
Enhancement
search-results-image.tsx
Replace glassmorphic with Tailwind utilities                         

components/search-results-image.tsx

  • Removed glassmorphic CSS import statement
  • Replaced .glassmorphic class on image thumbnail wrapper with
    backdrop-blur-md bg-background/30 border border-border/40 shadow-lg
    rounded-md overflow-hidden
  • Made nested Card component transparent with bg-transparent border-none
    shadow-none classes
  • Replaced .glassmorphic class on DialogContent with backdrop-blur-md
    bg-background/80 border-border/40 shadow-lg
+3/-4     
Dependencies
package.json
Remove glassmorphic package dependency                                     

package.json

  • Removed glassmorphic npm package dependency (version ^0.0.3)
+0/-1     

Summary by CodeRabbit

  • New Features

    • History sidebar, Usage dashboard/view, Purchase Credits popup, Credits preview, and clickable resolution image previews.
    • Usage/History/Profile toggles added to header and mobile controls; related UI panels integrated.
  • Style

    • Refined thumbnail and modal visuals using backdrop blur, updated borders/shadows, and cleaner card/dialog surfaces.
  • Behavior

    • Map previews and GeoJSON rendering added; drawn map features are now included in submissions.
    • Map navigation controls tied to desktop/drawing mode; minor test adjustments.

- Removed 'glassmorphic' npm package and CSS import.
- Replaced '.glassmorphic' class with Tailwind 'backdrop-blur-md', theme-aware semi-transparent backgrounds, and borders in 'components/search-results-image.tsx'.
- Updated nested Card components to be transparent to ensure the glassmorphic effect is visible.
- Re-synced 'bun.lock' after removing the dependency.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
@google-labs-jules
Copy link
Contributor

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel
Copy link
Contributor

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
qcx Ready Ready Preview, Comment Feb 4, 2026 5:11pm

@charliecreates charliecreates bot requested a review from CharlieHelps February 4, 2026 09:07
@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 4, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Walkthrough

Removes the glassmorphic dependency and refactors UI surfaces and state: adds Usage and History toggle contexts and views, a HistorySidebar and PurchaseCreditsPopup, integrates usage/history toggles into header/profile/chat flows, includes drawnFeatures in multipart submissions and AI prompts, adds ResolutionImage/geojson preview support in actions, adjusts Mapbox NavigationControl lifecycle for drawing mode, and updates related tests.

Changes

Cohort / File(s) Summary
Toggle Providers & Views
components/usage-toggle-context.tsx, components/history-toggle-context.tsx, components/usage-view.tsx, components/history-sidebar.tsx, components/purchase-credits-popup.tsx
Add Usage and History toggle contexts/providers and new UI components: UsageView, HistorySidebar, PurchaseCreditsPopup to surface usage/history and purchase flows.
Header / Profile / Chat Coordination
components/header.tsx, components/header-search-button.tsx, components/profile-toggle-context.tsx, components/profile-toggle.tsx, components/history.tsx, components/chat.tsx, components/conditional-lottie.tsx, components/mobile-icons-bar.tsx, components/sidebar/chat-history-client.tsx, components/chat-panel.tsx
Wire usage/history toggles into header, profile, and chat UIs; add purchase popup launch; change Stripe link(s); conditionally render UsageView; add credits preview in history; include drawnFeatures in form submissions; tweak Lottie visibility and search static map scale.
Mapbox / Drawing Controls
components/map/mapbox-map.tsx, tests/map.spec.ts
Introduce navControlRef and make NavigationControl lifecycle conditional (desktop + drawing mode); remove control on mode change/unmount; update test to enable drawing mode before asserting zoom control.
App-level actions & UI streaming
app/actions.tsx, app/layout.tsx
Parse and forward drawnFeatures in actions; sanitize AI message history (replace image parts), extend result payloads with image/geojson and render ResolutionImage/GeoJsonLayer; switch some uiStream.append calls to uiStream.update; add UsageToggleProvider and HistoryToggleProvider to layout and render HistorySidebar.
Search UI & Resolution Preview
components/resolution-image.tsx, components/search-results-image.tsx, components/search-related.tsx
Add ResolutionImage component (thumbnail → dialog); remove glassmorphic.css and replace glass styling with backdrop-blur/background/border/shadow classes; change related-query submission from form submit to onClick handler.
Agents & Geospatial tooling
lib/agents/researcher.tsx, lib/agents/resolution-search.tsx, lib/agents/tools/geospatial.tsx
Expose drawnFeatures in researcher/getDefaultSystemPrompt and embed explicit geometry in resolution-search prompt; add helper to populate Google Static Map URLs into MCP responses when available.
Misc / Cleanup / Tests
package.json, lib/utils/index.ts, tests/map.spec.ts, components/followup-panel.tsx, components/chat.tsx
Remove glassmorphic dependency and unused imports, update tests to reflect drawing-mode control visibility, add drawnFeatures to followup-panel form data, and small submit/timeout tweaks in chat.

Sequence Diagram(s)

(Skipped)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Review effort 3/5

Suggested reviewers

  • CharlieHelps

Poem

I hopped through props and toggles bright,
Swapped glass for blur in morning light,
Opened sidebars, credits, purchase cheer,
Sent drawn shapes to prompts so the map draws near,
A rabbit nods — the UI’s clearer here. 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Replace glassmorphic with Tailwind utilities' directly and clearly describes the main change in the PR: removing the external glassmorphic dependency and replacing its styling with Tailwind CSS utilities. This is confirmed by the detailed changes in components/search-results-image.tsx and package.json.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch replace-glassmorphic-with-tailwind-9822029702985678452

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Feb 4, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Add missing border, rounding, overflow

Add the border, rounded-md, and overflow-hidden classes to the DialogContent
component to correctly apply the intended glassmorphic styling.

components/search-results-image.tsx [97]

-<DialogContent className="sm:max-w-3xl max-h-[80vh] overflow-auto backdrop-blur-md bg-background/80 border-border/40 shadow-lg">
+<DialogContent className="sm:max-w-3xl max-h-[80vh] overflow-auto backdrop-blur-md bg-background/80 border border-border/40 shadow-lg rounded-md overflow-hidden">
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the border utility is missing for the border to appear, and that rounded-md and overflow-hidden are needed for visual consistency with other elements in the file.

Medium
  • Update

Copy link

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

Overall the Tailwind replacement is reasonable, but there are two concrete problems: an unnecessary any in the .map() callback reduces type safety, and DialogContent uses border-border/40 without border, likely preventing the intended border from rendering (visual regression). Address these to keep the refactor maintainable and the UI consistent.

Additional notes (1)
  • Maintainability | components/search-results-image.tsx:66-66
    images.slice(0, 4).map((image: any, index: number) => ...) keeps an explicit any in a user-facing UI component. Even if it compiles, this is a clear footgun: it hides data-shape mistakes and makes it easier to accidentally pass non-strings into src, which can lead to confusing runtime behavior (e.g., [object Object] URLs or unexpected coercions). Since the component prop is already images: string[], the any is unnecessary and should be removed to keep type safety intact.
Summary of changes

What changed

  • Removed the glassmorphic dependency and its CSS import:

    • Dropped import 'glassmorphic/glassmorphic.css' from components/search-results-image.tsx.
    • Removed "glassmorphic" from package.json dependencies (lockfile updated accordingly).
  • Replaced .glassmorphic usage with Tailwind utilities in SearchResultsImageSection:

    • Thumbnail wrapper now uses backdrop-blur-md, semi-transparent bg-background/*, border border-border/40, shadow-lg, rounded-md, and overflow-hidden.
    • The nested Card is made visually transparent via bg-transparent border-none shadow-none to let the wrapper’s blur/background show through.
    • DialogContent now uses backdrop-blur-md bg-background/80 border-border/40 shadow-lg instead of .glassmorphic.

…(synced with main)

- Removed 'glassmorphic' npm package and CSS import.
- Replaced '.glassmorphic' class with Tailwind 'backdrop-blur-md', theme-aware semi-transparent backgrounds, and borders in 'components/search-results-image.tsx'.
- Updated nested Card components to be transparent to ensure the glassmorphic effect is visible.
- Re-synced 'bun.lock' after removing the dependency.
- Merged latest changes from origin/main.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
components/mobile-icons-bar.tsx (1)

51-55: 🧹 Nitpick | 🔵 Trivial

Consider extracting the Stripe URL to a shared constant.

The same Stripe checkout URL appears in multiple components (here and in usage-view.tsx line 35). Centralizing this URL in a constants file or environment variable would make updates easier and reduce the risk of inconsistent links.

💡 Suggested approach

Create a shared constant:

// lib/constants.ts
export const STRIPE_CHECKOUT_URL = 'https://buy.stripe.com/14A3cv7K72TR3go14Nasg02'

Then import and use it in both components.

app/actions.tsx (1)

672-687: ⚠️ Potential issue | 🔴 Critical

Wrap switch cases to fix noSwitchDeclarations errors.

The 'assistant' and 'related' cases contain const declarations without block scoping, which violates Biome's noSwitchDeclarations rule. Wrapping these cases in braces prevents potential scope bleed and resolves the lint error. The 'resolution_search_result' case already has proper block scoping.

Proposed fix
-        case 'assistant':
-          const answer = createStreamableValue(content as string)
-          answer.done(content as string)
-          switch (type) {
+        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: []
-              })
-              relatedQueries.done(JSON.parse(content as string))
-              return {
-                id,
-                component: (
-                  <Section title="Related" separator={true}>
-                    <SearchRelated relatedQueries={relatedQueries.value} />
-                  </Section>
-                )
-              }
+            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':
               return {
                 id,
                 component: (
                   <Section title="Follow-up" className="pb-8">
                     <FollowupPanel />
                   </Section>
                 )
               }
             case 'resolution_search_result': {
               const analysisResult = JSON.parse(content as string);
               const geoJson = analysisResult.geoJson as FeatureCollection;

               return {
                 id,
                 component: (
                   <>
                     {geoJson && (
                       <GeoJsonLayer id={id} data={geoJson} />
                     )}
                   </>
                 )
               }
             }
           }
-          break
+          break
+        }
components/header.tsx (1)

54-67: 🧹 Nitpick | 🔵 Trivial

Logo button as history toggle may reduce discoverability.

Using the logo as a toggle for the history sidebar is unconventional. Users typically expect clicking a logo to navigate home. Consider:

  1. Adding a tooltip (e.g., title="Open History") to indicate the action
  2. Adding an aria-label for accessibility
♿ Suggested accessibility improvement
-        <Button variant="ghost" size="icon" onClick={toggleHistory} data-testid="logo-history-toggle">
+        <Button variant="ghost" size="icon" onClick={toggleHistory} data-testid="logo-history-toggle" title="Open History" aria-label="Open History">
🤖 Fix all issues with AI agents
In `@app/layout.tsx`:
- Line 93: HistorySidebar is rendered unconditionally and shows an error for
unauthenticated users because ChatHistoryClient delegates auth to the API;
update the layout to conditionally render HistorySidebar only for authenticated
users (or add a guard inside HistorySidebar) by reusing the same auth check used
in app/search/[id]/page.tsx — call the project’s current user/session helper
(e.g., getCurrentUser/requireAuth or the existing auth utility) and render
<HistorySidebar /> only when that check confirms a logged-in user, otherwise
render nothing or redirect; alternatively, implement a component-level guard
inside HistorySidebar to early-return null when unauthenticated (detect 401 from
ChatHistoryClient) so the panel is hidden for unauth users.

In `@components/header.tsx`:
- Around line 80-82: The TentTree toggle Buttons (the Button elements using
TentTree and onClick={handleUsageToggle}) need accessible attributes: add an
aria-label (e.g., "Toggle usage panel") and an appropriate toggle state
attribute such as aria-pressed tied to the component state that indicates
whether the usage panel is open (use your existing usage-open state variable
used by handleUsageToggle), and optionally a title for hover; apply the same
changes to both the desktop Button and the mobile Button instances so screen
readers receive descriptive text and the current pressed/state is exposed.
- Around line 39-42: The unconditional setIsPurchaseOpen(true) in the useEffect
should be gated by an authentication check and a 7-day cooldown: inside the
useEffect that currently calls setIsPurchaseOpen, first verify the user is
authenticated (use your auth context/prop such as currentUser or
isAuthenticated) and then read a timestamp from localStorage (e.g., key
"lastPurchasePopupShown"); only call setIsPurchaseOpen(true) if authenticated
and either no timestamp exists or now - timestamp >= 7 days, and when you open
the popup update localStorage with Date.now(); otherwise do nothing so
PurchaseCreditsPopup will not open.

In `@components/purchase-credits-popup.tsx`:
- Around line 21-23: The handlePurchase handler in
components/purchase-credits-popup.tsx opens Stripe checkout with
window.open(url, '_blank') which is vulnerable to reverse-tabnabbing; update the
call in handlePurchase to open the window with noopener and noreferrer
protection (use the third argument of window.open or nullify opener on the
returned window) so it matches the secure pattern used in
components/mobile-icons-bar.tsx, and make the same change for the similar
window.open usage in components/usage-view.tsx.

In `@components/usage-toggle-context.tsx`:
- Around line 13-24: The context value object in UsageToggleProvider is
recreated on every render causing needless consumer re-renders; wrap the value
({ isUsageOpen, toggleUsage, closeUsage }) in useMemo so it only changes when
isUsageOpen (or the handlers change) and pass that memoized object to
UsageToggleContext.Provider; reference the UsageToggleProvider component and the
value fields isUsageOpen, toggleUsage, closeUsage when making the change.

In `@components/usage-view.tsx`:
- Around line 10-15: The variables usage and credits are declared with React
useState but never updated; replace useState([ ... ]) and useState(0) with plain
constants (e.g., const usage = [...] and const credits = 0) to avoid unnecessary
state overhead in the component, and optionally add a TODO comment near
usage/credits if you intend to make them dynamic later; update any imports if
useState becomes unused and remove it from the file.
- Around line 82-89: Replace the array-index key used in the TableRow within
usage.map (currently key={i}) with a stable unique identifier from each usage
item (e.g., item.id or another unique field) by updating the data model to
include a unique id for each entry and changing the key to use that field;
locate the usage.map block in components/usage-view.tsx and modify both the
source usage array (add id) and the TableRow to use item.id (or another unique
property) instead of the index to ensure stable keys during re-renders.
- Line 3: The import statement currently brings in useEffect and useState but
useEffect is unused; update the import at the top of the component to remove
useEffect (keep React and useState) so the line imports only the symbols
actually used and eliminates the unused-import lint warning.

In `@tests/map.spec.ts`:
- Around line 38-41: The visibility assertion for the zoom control (locator
'.mapboxgl-ctrl-zoom-in') can be flaky after toggling drawing mode via
page.click('[data-testid="map-toggle"]') and
page.click('[data-testid="map-mode-draw"]'); update the test to wait explicitly
for the control with an extended timeout (e.g., use
expect(page.locator('.mapboxgl-ctrl-zoom-in')).toBeVisible with a higher timeout
or await page.waitForSelector('.mapboxgl-ctrl-zoom-in', { timeout: ... })) so
the assertion only runs after the navigation control has had time to render.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac3e9c5 and 789e2fb.

⛔ Files ignored due to path filters (1)
  • CC BY-NC 4.0.docx is excluded by !**/*.docx
📒 Files selected for processing (19)
  • app/actions.tsx
  • app/layout.tsx
  • components/chat-panel.tsx
  • components/chat.tsx
  • components/conditional-lottie.tsx
  • components/header-search-button.tsx
  • components/header.tsx
  • components/history-sidebar.tsx
  • components/history-toggle-context.tsx
  • components/history.tsx
  • components/map/mapbox-map.tsx
  • components/mobile-icons-bar.tsx
  • components/profile-toggle-context.tsx
  • components/profile-toggle.tsx
  • components/purchase-credits-popup.tsx
  • components/sidebar/chat-history-client.tsx
  • components/usage-toggle-context.tsx
  • components/usage-view.tsx
  • tests/map.spec.ts
💤 Files with no reviewable changes (1)
  • components/header-search-button.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
📚 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/usage-view.tsx
  • components/purchase-credits-popup.tsx
  • components/sidebar/chat-history-client.tsx
  • components/header.tsx
🧬 Code graph analysis (10)
tests/map.spec.ts (1)
tests/responsive.spec.ts (3)
  • test (3-40)
  • test (262-297)
  • test (94-191)
components/profile-toggle-context.tsx (1)
components/settings/settings-view.tsx (1)
  • activeView (12-16)
components/usage-view.tsx (1)
components/usage-toggle-context.tsx (1)
  • useUsageToggle (26-30)
components/purchase-credits-popup.tsx (2)
components/ui/dialog.tsx (2)
  • Dialog (112-112)
  • props (35-53)
components/ui/button.tsx (1)
  • Button (56-56)
components/history-toggle-context.tsx (1)
components/map-toggle-context.tsx (1)
  • mapToggleState (22-34)
components/sidebar/chat-history-client.tsx (1)
components/history-toggle-context.tsx (1)
  • useHistoryToggle (26-30)
components/usage-toggle-context.tsx (1)
components/map-toggle-context.tsx (1)
  • mapToggleState (22-34)
app/layout.tsx (9)
components/history-toggle-context.tsx (1)
  • HistoryToggleProvider (13-24)
components/map-toggle-context.tsx (1)
  • MapToggleProvider (22-34)
components/profile-toggle-context.tsx (1)
  • ProfileToggleProvider (25-41)
components/usage-toggle-context.tsx (1)
  • UsageToggleProvider (13-24)
components/theme-provider.tsx (1)
  • ThemeProvider (7-9)
components/map-loading-context.tsx (1)
  • MapLoadingProvider (11-18)
components/header.tsx (1)
  • Header (24-100)
components/sidebar.tsx (1)
  • Sidebar (6-12)
components/history-sidebar.tsx (1)
  • HistorySidebar (15-35)
app/actions.tsx (1)
lib/agents/inquire.tsx (1)
  • inquire (12-54)
components/profile-toggle.tsx (3)
components/profile-toggle-context.tsx (1)
  • useProfileToggle (43-49)
components/usage-toggle-context.tsx (1)
  • useUsageToggle (26-30)
components/ui/dropdown-menu.tsx (1)
  • DropdownMenuItem (212-212)
🪛 Biome (2.3.13)
app/actions.tsx

[error] 672-672: 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] 685-687: 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)

🔇 Additional comments (18)
components/chat-panel.tsx (1)

133-157: LGTM!

Adding setSuggestions to the dependency array is the correct fix. Since setSuggestions is used within the debounced callback, including it ensures the closure always has the latest reference, preventing stale closure bugs.

components/conditional-lottie.tsx (1)

6-14: LGTM!

The visibility logic extension is clean and consistent with the existing pattern. Hiding the Lottie animation when the usage panel is open prevents visual clutter and ensures proper UI layering.

components/profile-toggle-context.tsx (1)

32-37: LGTM!

The closeProfileView method provides a clean, explicit API for closing the profile view without needing to know the current active section. This is useful for cross-context coordination (e.g., when usage panel opens).

components/profile-toggle.tsx (1)

37-43: LGTM!

The coordination logic correctly closes the usage panel only when opening a new profile section (not when closing one). This provides a good UX where panels don't overlap.

Note: The coordination is currently one-way (profile toggle closes usage). If the inverse behavior is also desired (usage toggle closes profile), you may want to add similar logic in the usage trigger component using closeProfileView.

components/chat.tsx (2)

16-37: Usage toggle state is wired cleanly.
State is read once and reused consistently; no issues.


113-114: View switching stays aligned across mobile and desktop.
Keeping the same branch order prevents layout divergence.

Also applies to: 224-224

app/actions.tsx (1)

726-742: Deterministic tool streamables look good.
Explicit initialization avoids undefined UI state; please verify against the current ai/rsc streamable value contract.

components/purchase-credits-popup.tsx (1)

35-70: Popup content structure is clear and scannable.

components/map/mapbox-map.tsx (1)

24-291: Navigation control lifecycle handling looks consistent.
Add/remove paths are now symmetric when entering/leaving drawing mode. Please verify against the Mapbox GL JS control API for the current version.

Also applies to: 503-533

components/history-toggle-context.tsx (1)

1-30: Toggle module API is clean and minimal.

components/history.tsx (1)

1-24: History button now delegates to the toggle cleanly.

components/sidebar/chat-history-client.tsx (2)

21-69: Deferring the history load until the panel opens is a nice optimization.


123-148: Credits preview toggle keeps the panel compact.

components/history-sidebar.tsx (2)

15-34: LGTM!

The component correctly uses the useHistoryToggle hook to manage sheet state, and the Suspense boundary with HistorySkeleton fallback is a good pattern for lazy-loading the chat history content.


12-12: Potential typo in import path: history-skeltonhistory-skeleton.

The import references './history-skelton' which appears to be a misspelling of "skeleton". Verify the actual filename exists with this spelling, or correct it to './history-skeleton'.

#!/bin/bash
# Check if the file exists with either spelling
fd -t f 'history-skel' components/
app/layout.tsx (1)

76-102: LGTM on provider structure.

The provider hierarchy is well-organized with toggle contexts wrapping the content that consumes them. The nesting order allows components to access all required contexts.

tests/map.spec.ts (1)

25-61: LGTM on test adaptation for Drawing Mode behavior.

The test correctly reflects the new behavior where zoom controls are only visible in Drawing Mode on desktop. The precondition checks and skip conditions are appropriate.

components/header.tsx (1)

31-37: LGTM on usage/profile coordination logic.

The handleUsageToggle function correctly closes the profile view before opening the usage panel, preventing overlapping sidebars. This is good UX coordination.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

<ConditionalLottie />
{children}
<Sidebar />
<HistorySidebar />
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find the files mentioned in the review
fd -type f -name "layout.tsx" | head -5

Repository: QueueLab/QCX

Length of output: 226


🏁 Script executed:

# Find HistorySidebar component
fd -type f -name "*history*sidebar*"

Repository: QueueLab/QCX

Length of output: 226


🏁 Script executed:

# Find ChatHistoryClient component
fd -type f -name "*chat-history*client*"

Repository: QueueLab/QCX

Length of output: 226


🏁 Script executed:

# Get context around line 93 in app/layout.tsx
if [ -f "app/layout.tsx" ]; then
  sed -n '80,110p' app/layout.tsx
fi

Repository: QueueLab/QCX

Length of output: 1053


🏁 Script executed:

# Search for HistorySidebar and related files
find . -name "*sidebar*" -o -name "*history*" | grep -i history | head -20

Repository: QueueLab/QCX

Length of output: 357


🏁 Script executed:

# Search for ChatHistoryClient
find . -name "*chat*" | grep -E "(history|client)" | head -20

Repository: QueueLab/QCX

Length of output: 100


🏁 Script executed:

# Look for the actual component definitions using rg
rg "export.*HistorySidebar|function HistorySidebar|const HistorySidebar" -t ts -t tsx --max-count 5

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Look for ChatHistoryClient
rg "export.*ChatHistoryClient|function ChatHistoryClient|const ChatHistoryClient" -t ts -t tsx --max-count 5

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Read HistorySidebar component
cat components/history-sidebar.tsx

Repository: QueueLab/QCX

Length of output: 1124


🏁 Script executed:

# Read ChatHistoryClient component
cat components/sidebar/chat-history-client.tsx

Repository: QueueLab/QCX

Length of output: 7606


🏁 Script executed:

# Find the /api/chats endpoint
find . -path "*/api/chats*" -type f

Repository: QueueLab/QCX

Length of output: 109


🏁 Script executed:

# Search for getCurrentUserId to understand how auth is handled at API level
rg "getCurrentUserId" -A 10 --max-count 3

Repository: QueueLab/QCX

Length of output: 12675


🏁 Script executed:

# Check if there are other components that use useAuth or useSession for authentication
rg "useAuth|useSession|getCurrentUser" components/ -l | head -10

Repository: QueueLab/QCX

Length of output: 98


Add authentication guard to prevent unauth users from seeing error state.

The HistorySidebar is rendered unconditionally in the layout, but the ChatHistoryClient delegates authentication to the API level (/api/chats returns 401 if unauthorized). This means unauthenticated users will see the History panel with an error message instead of no panel at all.

For consistency with other authenticated pages like app/search/[id]/page.tsx (which redirects unauthenticated users), either wrap HistorySidebar in an authentication check within the layout or add a component-level guard in HistorySidebar that prevents rendering when unauthorized.

🤖 Prompt for AI Agents
In `@app/layout.tsx` at line 93, HistorySidebar is rendered unconditionally and
shows an error for unauthenticated users because ChatHistoryClient delegates
auth to the API; update the layout to conditionally render HistorySidebar only
for authenticated users (or add a guard inside HistorySidebar) by reusing the
same auth check used in app/search/[id]/page.tsx — call the project’s current
user/session helper (e.g., getCurrentUser/requireAuth or the existing auth
utility) and render <HistorySidebar /> only when that check confirms a logged-in
user, otherwise render nothing or redirect; alternatively, implement a
component-level guard inside HistorySidebar to early-return null when
unauthenticated (detect 401 from ChatHistoryClient) so the panel is hidden for
unauth users.

Comment on lines +39 to +42
useEffect(() => {
// Open payment popup as soon as application opens
setIsPurchaseOpen(true)
}, [])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for PurchaseCreditsPopup component file
fd -t f 'purchase.*popup' --ignore-case

Repository: QueueLab/QCX

Length of output: 93


🏁 Script executed:

# Check the full header.tsx to understand auth context usage
find . -name "header.tsx" -type f | head -5 | xargs -I {} wc -l {}

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Search for auth context or session usage patterns
rg -l 'useAuth|useSession|authenticated|auth.*context' --type tsx --type ts | head -20

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Read the PurchaseCreditsPopup component to check for auth/cooldown handling
cat -n components/purchase-credits-popup.tsx

Repository: QueueLab/QCX

Length of output: 2832


🏁 Script executed:

# Read the full header.tsx to see auth context and state management
cat -n components/header.tsx

Repository: QueueLab/QCX

Length of output: 4024


🏁 Script executed:

# Search for cooldown or 7-day patterns
rg -n '7.*day|cooldown|COOLDOWN|getTime|localStorage.*cooldown' --type-list | head -20

Repository: QueueLab/QCX

Length of output: 445


🏁 Script executed:

# Search for useAuth or auth context imports
rg -n 'useAuth|useSession|AuthContext|authContext' | head -30

Repository: QueueLab/QCX

Length of output: 38


Add authentication check and 7-day cooldown before opening payment popup.

The PurchaseCreditsPopup component contains no authentication or cooldown logic. The unconditional setIsPurchaseOpen(true) on mount violates the intended behavior: the popup should only appear after user authentication and respect a 7-day cooldown period.

Add:

  1. Authentication guard (check if user is authenticated)
  2. 7-day cooldown tracking (e.g., via localStorage with timestamp comparison)

The popup should only open on mount if both conditions are met.

🤖 Prompt for AI Agents
In `@components/header.tsx` around lines 39 - 42, The unconditional
setIsPurchaseOpen(true) in the useEffect should be gated by an authentication
check and a 7-day cooldown: inside the useEffect that currently calls
setIsPurchaseOpen, first verify the user is authenticated (use your auth
context/prop such as currentUser or isAuthenticated) and then read a timestamp
from localStorage (e.g., key "lastPurchasePopupShown"); only call
setIsPurchaseOpen(true) if authenticated and either no timestamp exists or now -
timestamp >= 7 days, and when you open the popup update localStorage with
Date.now(); otherwise do nothing so PurchaseCreditsPopup will not open.

Comment on lines +80 to +82
<Button variant="ghost" size="icon" onClick={handleUsageToggle}>
<TentTree className="h-[1.2rem] w-[1.2rem]" />
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding accessibility attributes to usage toggle buttons.

The TentTree buttons lack descriptive attributes for screen readers.

♿ Suggested fix
-        <Button variant="ghost" size="icon" onClick={handleUsageToggle}>
+        <Button variant="ghost" size="icon" onClick={handleUsageToggle} title="Open Usage" aria-label="Open Usage">
           <TentTree className="h-[1.2rem] w-[1.2rem]" />
         </Button>

Apply to both desktop (Line 80) and mobile (Line 92) buttons.

Also applies to: 92-94

🤖 Prompt for AI Agents
In `@components/header.tsx` around lines 80 - 82, The TentTree toggle Buttons (the
Button elements using TentTree and onClick={handleUsageToggle}) need accessible
attributes: add an aria-label (e.g., "Toggle usage panel") and an appropriate
toggle state attribute such as aria-pressed tied to the component state that
indicates whether the usage panel is open (use your existing usage-open state
variable used by handleUsageToggle), and optionally a title for hover; apply the
same changes to both the desktop Button and the mobile Button instances so
screen readers receive descriptive text and the current pressed/state is
exposed.

Comment on lines +21 to +23
const handlePurchase = () => {
window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank');
onClose();
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and view the content around lines 21-23
if [ -f "components/purchase-credits-popup.tsx" ]; then
  echo "=== File exists, showing content with line numbers ==="
  cat -n components/purchase-credits-popup.tsx | head -30
else
  echo "=== File not found at components/purchase-credits-popup.tsx ==="
  # Try to find it
  fd purchase-credits-popup.tsx
fi

Repository: QueueLab/QCX

Length of output: 1041


🏁 Script executed:

# Verify the window.open syntax and best practices for noopener
# Let's check if there are any other window.open calls in the codebase to see patterns
rg "window\.open" --type tsx --type ts -A 1

Repository: QueueLab/QCX

Length of output: 83


🏁 Script executed:

# Search for window.open patterns in the codebase
rg "window\.open" -A 1 --max-count 20

Repository: QueueLab/QCX

Length of output: 449


🏁 Script executed:

# Let's check the complete usage-view.tsx line to see if it has the same issue
rg "window\.open" -B 2 -A 2 components/usage-view.tsx

Repository: QueueLab/QCX

Length of output: 379


🏁 Script executed:

# Let's verify the security best practice - check if there's any documentation in the repo
# about window.open patterns or security practices
find . -name "*.md" -o -name "*.txt" | head -20 | xargs grep -l "window.open" 2>/dev/null || echo "No docs found mentioning window.open"

# Also check if there's a security policy or guidelines
ls -la | grep -i security
fd -e md -e txt | xargs grep -l "noopener\|noreferrer" 2>/dev/null || echo "No existing references to noopener/noreferrer"

Repository: QueueLab/QCX

Length of output: 138


🏁 Script executed:

# Let's verify the exact window.open behavior and security concern
# Check if there's any use of external links with proper security attributes
rg "rel=" | grep -i "noopener\|noreferrer" | head -5

Repository: QueueLab/QCX

Length of output: 193


🏁 Script executed:

# Let's check the mobile-icons-bar.tsx to understand how the codebase handles external links
cat -n components/mobile-icons-bar.tsx | grep -A 2 -B 2 "buy.stripe"

Repository: QueueLab/QCX

Length of output: 435


Add noopener,noreferrer when opening Stripe checkout.

Opening a new tab with window.open('...', '_blank') without security parameters enables reverse-tabnabbing, allowing the opened page to redirect the original tab. The codebase properly uses rel="noopener noreferrer" for the same Stripe link in components/mobile-icons-bar.tsx, but this instance needs the same protection.

Proposed fix
  const handlePurchase = () => {
-   window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank');
+   window.open(
+     'https://buy.stripe.com/14A3cv7K72TR3go14Nasg02',
+     '_blank',
+     'noopener,noreferrer'
+   );
    onClose();

Note: The same vulnerability exists in components/usage-view.tsx and should also be fixed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handlePurchase = () => {
window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank');
onClose();
const handlePurchase = () => {
window.open(
'https://buy.stripe.com/14A3cv7K72TR3go14Nasg02',
'_blank',
'noopener,noreferrer'
);
onClose();
🤖 Prompt for AI Agents
In `@components/purchase-credits-popup.tsx` around lines 21 - 23, The
handlePurchase handler in components/purchase-credits-popup.tsx opens Stripe
checkout with window.open(url, '_blank') which is vulnerable to
reverse-tabnabbing; update the call in handlePurchase to open the window with
noopener and noreferrer protection (use the third argument of window.open or
nullify opener on the returned window) so it matches the secure pattern used in
components/mobile-icons-bar.tsx, and make the same change for the similar
window.open usage in components/usage-view.tsx.

Comment on lines +13 to +24
export const UsageToggleProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [isUsageOpen, setIsUsageOpen] = useState(false)

const toggleUsage = () => setIsUsageOpen(prev => !prev)
const closeUsage = () => setIsUsageOpen(false)

return (
<UsageToggleContext.Provider value={{ isUsageOpen, toggleUsage, closeUsage }}>
{children}
</UsageToggleContext.Provider>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider memoizing the context value to prevent unnecessary re-renders.

The context value object is recreated on every render, which can cause all consumers to re-render even when the actual values haven't changed.

♻️ Proposed fix
+'use client'
+
+import { createContext, useContext, useState, ReactNode, useMemo, useCallback } from "react"
+
 export const UsageToggleProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
   const [isUsageOpen, setIsUsageOpen] = useState(false)
 
-  const toggleUsage = () => setIsUsageOpen(prev => !prev)
-  const closeUsage = () => setIsUsageOpen(false)
+  const toggleUsage = useCallback(() => setIsUsageOpen(prev => !prev), [])
+  const closeUsage = useCallback(() => setIsUsageOpen(false), [])
+
+  const value = useMemo(
+    () => ({ isUsageOpen, toggleUsage, closeUsage }),
+    [isUsageOpen, toggleUsage, closeUsage]
+  )
 
   return (
-    <UsageToggleContext.Provider value={{ isUsageOpen, toggleUsage, closeUsage }}>
+    <UsageToggleContext.Provider value={value}>
       {children}
     </UsageToggleContext.Provider>
   )
 }
🤖 Prompt for AI Agents
In `@components/usage-toggle-context.tsx` around lines 13 - 24, The context value
object in UsageToggleProvider is recreated on every render causing needless
consumer re-renders; wrap the value ({ isUsageOpen, toggleUsage, closeUsage })
in useMemo so it only changes when isUsageOpen (or the handlers change) and pass
that memoized object to UsageToggleContext.Provider; reference the
UsageToggleProvider component and the value fields isUsageOpen, toggleUsage,
closeUsage when making the change.

@@ -0,0 +1,96 @@
'use client'

import React, { useEffect, useState } from 'react'
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Remove unused import.

useEffect is imported but not used in this component.

🧹 Proposed fix
-import React, { useEffect, useState } from 'react'
+import React, { useState } from 'react'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React, { useEffect, useState } from 'react'
import React, { useState } from 'react'
🤖 Prompt for AI Agents
In `@components/usage-view.tsx` at line 3, The import statement currently brings
in useEffect and useState but useEffect is unused; update the import at the top
of the component to remove useEffect (keep React and useState) so the line
imports only the symbols actually used and eliminates the unused-import lint
warning.

Comment on lines +10 to +15
const [usage] = useState([
{ details: 'QCX-TERRA Crop yield Analysis', date: 'upcoming', change: 7 },
{ details: 'QCX-TERRA Flood predictions', date: 'upcoming', change: 5 },
{ details: 'Planet computer weather synchronization', date: 'upcoming', change: 3 },
])
const [credits] = useState(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Static data doesn't need useState.

The usage and credits values are never updated, so useState adds unnecessary overhead. If this is placeholder code for future dynamic data, consider adding a TODO comment.

♻️ Proposed fix for static data
 export function UsageView() {
-  const [usage] = useState([
+  const usage = [
     { details: 'QCX-TERRA Crop yield Analysis', date: 'upcoming', change: 7 },
     { details: 'QCX-TERRA Flood predictions', date: 'upcoming', change: 5 },
     { details: 'Planet computer weather synchronization', date: 'upcoming', change: 3 },
-  ])
-  const [credits] = useState(0)
+  ]
+  // TODO: Fetch actual credits from API
+  const credits = 0
   const { toggleUsage } = useUsageToggle()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [usage] = useState([
{ details: 'QCX-TERRA Crop yield Analysis', date: 'upcoming', change: 7 },
{ details: 'QCX-TERRA Flood predictions', date: 'upcoming', change: 5 },
{ details: 'Planet computer weather synchronization', date: 'upcoming', change: 3 },
])
const [credits] = useState(0)
const usage = [
{ details: 'QCX-TERRA Crop yield Analysis', date: 'upcoming', change: 7 },
{ details: 'QCX-TERRA Flood predictions', date: 'upcoming', change: 5 },
{ details: 'Planet computer weather synchronization', date: 'upcoming', change: 3 },
]
// TODO: Fetch actual credits from API
const credits = 0
🤖 Prompt for AI Agents
In `@components/usage-view.tsx` around lines 10 - 15, The variables usage and
credits are declared with React useState but never updated; replace useState([
... ]) and useState(0) with plain constants (e.g., const usage = [...] and const
credits = 0) to avoid unnecessary state overhead in the component, and
optionally add a TODO comment near usage/credits if you intend to make them
dynamic later; update any imports if useState becomes unused and remove it from
the file.

Comment on lines +82 to +89
<TableBody>
{usage.map((item, i) => (
<TableRow key={i}>
<TableCell className="text-xs font-medium">{item.details}</TableCell>
<TableCell className="text-[10px] text-muted-foreground">{item.date}</TableCell>
<TableCell className="text-xs text-right font-medium">{item.change}</TableCell>
</TableRow>
))}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid using array index as React key.

Using array index as key can cause issues with component state and reconciliation if items are reordered, added, or removed. Consider using a unique identifier from the data.

♻️ Proposed fix
-              {usage.map((item, i) => (
-                <TableRow key={i}>
+              {usage.map((item) => (
+                <TableRow key={item.details}>
                   <TableCell className="text-xs font-medium">{item.details}</TableCell>

Or add unique IDs to your data structure:

const usage = [
  { id: 'crop-yield', details: 'QCX-TERRA Crop yield Analysis', ... },
  ...
]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TableBody>
{usage.map((item, i) => (
<TableRow key={i}>
<TableCell className="text-xs font-medium">{item.details}</TableCell>
<TableCell className="text-[10px] text-muted-foreground">{item.date}</TableCell>
<TableCell className="text-xs text-right font-medium">{item.change}</TableCell>
</TableRow>
))}
<TableBody>
{usage.map((item) => (
<TableRow key={item.details}>
<TableCell className="text-xs font-medium">{item.details}</TableCell>
<TableCell className="text-[10px] text-muted-foreground">{item.date}</TableCell>
<TableCell className="text-xs text-right font-medium">{item.change}</TableCell>
</TableRow>
))}
🤖 Prompt for AI Agents
In `@components/usage-view.tsx` around lines 82 - 89, Replace the array-index key
used in the TableRow within usage.map (currently key={i}) with a stable unique
identifier from each usage item (e.g., item.id or another unique field) by
updating the data model to include a unique id for each entry and changing the
key to use that field; locate the usage.map block in components/usage-view.tsx
and modify both the source usage array (add id) and the TableRow to use item.id
(or another unique property) instead of the index to ensure stable keys during
re-renders.

Comment on lines +38 to +41
// Zoom controls are now only visible in Drawing Mode on desktop
await page.click('[data-testid="map-toggle"]');
await page.click('[data-testid="map-mode-draw"]');
await expect(page.locator('.mapboxgl-ctrl-zoom-in')).toBeVisible();
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a timeout for Drawing Mode control visibility.

When switching to Drawing Mode, there may be a delay before the navigation controls are rendered. The current assertion uses the default timeout, which should work in most cases, but adding an explicit wait or extended timeout could improve test reliability.

💡 Optional: Add explicit timeout for flaky scenarios
     // Zoom controls are now only visible in Drawing Mode on desktop
     await page.click('[data-testid="map-toggle"]');
     await page.click('[data-testid="map-mode-draw"]');
-    await expect(page.locator('.mapboxgl-ctrl-zoom-in')).toBeVisible();
+    await expect(page.locator('.mapboxgl-ctrl-zoom-in')).toBeVisible({ timeout: 5000 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Zoom controls are now only visible in Drawing Mode on desktop
await page.click('[data-testid="map-toggle"]');
await page.click('[data-testid="map-mode-draw"]');
await expect(page.locator('.mapboxgl-ctrl-zoom-in')).toBeVisible();
// Zoom controls are now only visible in Drawing Mode on desktop
await page.click('[data-testid="map-toggle"]');
await page.click('[data-testid="map-mode-draw"]');
await expect(page.locator('.mapboxgl-ctrl-zoom-in')).toBeVisible({ timeout: 5000 });
🤖 Prompt for AI Agents
In `@tests/map.spec.ts` around lines 38 - 41, The visibility assertion for the
zoom control (locator '.mapboxgl-ctrl-zoom-in') can be flaky after toggling
drawing mode via page.click('[data-testid="map-toggle"]') and
page.click('[data-testid="map-mode-draw"]'); update the test to wait explicitly
for the control with an extended timeout (e.g., use
expect(page.locator('.mapboxgl-ctrl-zoom-in')).toBeVisible with a higher timeout
or await page.waitForSelector('.mapboxgl-ctrl-zoom-in', { timeout: ... })) so
the assertion only runs after the navigation control has had time to render.

…(synced with latest main)

- Removed 'glassmorphic' npm package and CSS import.
- Replaced '.glassmorphic' class with Tailwind 'backdrop-blur-md', theme-aware semi-transparent backgrounds, and borders in 'components/search-results-image.tsx'.
- Updated nested Card components to be transparent to ensure the glassmorphic effect is visible.
- Re-synced 'bun.lock' after removing the dependency.
- Reset to latest origin/main and re-applied changes.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
…(synchronized with main)

- Removed 'glassmorphic' npm package and CSS import.
- Replaced '.glassmorphic' class with Tailwind 'backdrop-blur-md', theme-aware semi-transparent backgrounds, and borders in 'components/search-results-image.tsx'.
- Updated nested Card components to be transparent to ensure the glassmorphic effect is visible.
- Re-synced 'bun.lock' after removing the dependency.
- Synchronized with the latest 'main' branch.

Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 (3)
components/search-related.tsx (1)

46-61: ⚠️ Potential issue | 🟡 Minor

Minor: onClick handler could receive empty string despite filter.

The filter on line 48 checks item?.query !== '', but the onClick on line 55 uses item?.query || '' which would pass an empty string if query is undefined. This is a minor edge case since the filter should prevent it, but the fallback is inconsistent with the filter logic.

Suggested improvement
             <Button
               variant="link"
               className="flex-1 justify-start px-0 py-1 h-fit font-semibold text-accent-foreground/50 whitespace-normal text-left"
-              onClick={() => handleRelatedClick(item?.query || '')}
+              onClick={() => item?.query && handleRelatedClick(item.query)}
             >
app/actions.tsx (2)

689-701: ⚠️ Potential issue | 🟡 Minor

Wrap switch case declarations in blocks to prevent scope leakage.

Static analysis correctly identifies that declarations inside switch cases can be accessed from other cases, which can lead to subtle bugs.

🛠️ Proposed fix for case 'response'
         case 'response':
+          {
+          const answer = createStreamableValue(content as string)
+          answer.done(content as string)
           return {
             id,
             component: (
               <Section title="response">
                 <BotMessage content={answer.value} />
               </Section>
             )
           }
+          }

Note: The answer variable is currently declared before the switch on the type, but it's only used in the 'response' case. Move the declaration inside the case block.


702-714: ⚠️ Potential issue | 🟡 Minor

Wrap switch case declarations in blocks.

The relatedQueries declaration should be scoped to this case to prevent accidental access from other cases.

🛠️ Proposed fix for case 'related'
-            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>
                 )
               }
+            }
🤖 Fix all issues with AI agents
In `@app/actions.tsx`:
- Around line 126-137: The code builds sanitizedHistory but then still passes
aiState.get().messages into aiState.done, so the sanitized image replacements
are never applied; update calls that use aiState.get().messages (notably the
aiState.done(...) invocation and any nearby usages around the end of the action)
to pass sanitizedHistory instead (or construct a finalMessages variable set to
sanitizedHistory and use that) so the sanitized content is what gets
saved/emitted; ensure you only change the argument passed to aiState.done (and
any other subsequent consumers) to reference sanitizedHistory rather than
aiState.get().messages.
- Around line 116-124: Extract the duplicated image-sanitization logic into two
small helper functions and replace the inline code around sanitizedMessages and
the subsequent image-placeholder block: create stripImageParts(messages:
CoreMessage[]) to remove parts with part.type === 'image' and use it where
sanitizedMessages is built, and create sanitizeImageData(messages: AIMessage[])
to map user messages and replace image fields with the "IMAGE_PROCESSED"
placeholder, then call that helper in the storage/history path; update callers
to use these helpers (referencing sanitizedMessages, stripImageParts, and
sanitizeImageData) to keep intent clear and remove duplication.

In `@components/chat.tsx`:
- Line 132: The duplicated nested ternary rendering logic for the right panel
should be extracted into a single helper (e.g., renderRightPanel) to avoid
repetition and ensure consistency; implement a function that returns
<SettingsView /> when activeView is truthy, <UsageView /> when isUsageOpen is
truthy, otherwise <MapProvider />, and replace both occurrences of the ternary
(the one using activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> :
<MapProvider />) with a single call to that helper in the component.

In `@components/resolution-image.tsx`:
- Around line 30-45: Remove the obsolete "glassmorphic" utility classes from the
component JSX (look for className on the motion.div wrapper and DialogContent)
and replace them with equivalent Tailwind utility classes used elsewhere in the
PR (e.g., backdrop-blur, bg-white/ bg-neutral with opacity, border/ ring, and
shadow utilities) so the visual styling is preserved; update className on the
motion.div containing Card and on the DialogContent component to use these
Tailwind utilities while keeping existing layout classes like rounded-lg,
overflow-hidden, max-h, p-1, etc.

In `@lib/agents/tools/geospatial.tsx`:
- Around line 159-161: getGoogleStaticMapUrl currently falls back to a
server-only key (GOOGLE_MAPS_API_KEY) and may embed it in client-visible URLs
(used by mcpData consumers); update getGoogleStaticMapUrl to only use
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY and never fall back to GOOGLE_MAPS_API_KEY,
returning an empty string or null when the public key is absent, and update any
call sites that expect a URL (e.g., where mcpData is populated) to handle the
missing URL gracefully; alternatively, if server-side rendering of the image is
preferred, move map generation into a backend endpoint that uses
GOOGLE_MAPS_API_KEY and returns the image or signed URL instead of exposing the
raw key.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 789e2fb and ea1503a.

📒 Files selected for processing (13)
  • app/actions.tsx
  • app_actions_diff.patch
  • components/chat-panel.tsx
  • components/chat.tsx
  • components/followup-panel.tsx
  • components/header-search-button.tsx
  • components/resolution-image.tsx
  • components/search-related.tsx
  • components_chat_patch.patch
  • lib/agents/researcher.tsx
  • lib/agents/resolution-search.tsx
  • lib/agents/tools/geospatial.tsx
  • lib/utils/index.ts
💤 Files with no reviewable changes (1)
  • lib/utils/index.ts
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
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.
🧬 Code graph analysis (6)
components/chat.tsx (2)
components/usage-toggle-context.tsx (1)
  • useUsageToggle (26-30)
components/usage-view.tsx (1)
  • UsageView (9-96)
components/search-related.tsx (2)
lib/schema/related.tsx (1)
  • PartialRelated (13-13)
components/ui/button.tsx (1)
  • Button (56-56)
components/resolution-image.tsx (3)
lib/utils/index.ts (1)
  • cn (10-12)
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/followup-panel.tsx (1)
components/map/map-data-context.tsx (1)
  • useMapData (51-57)
lib/agents/researcher.tsx (1)
lib/agents/resolution-search.tsx (1)
  • DrawnFeature (26-31)
app/actions.tsx (4)
lib/agents/resolution-search.tsx (1)
  • DrawnFeature (26-31)
components/map/geojson-layer.tsx (1)
  • GeoJsonLayer (13-100)
components/resolution-image.tsx (1)
  • ResolutionImage (22-60)
components/map/map-query-handler.tsx (1)
  • MapQueryHandler (30-82)
🪛 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)

🔇 Additional comments (16)
components_chat_patch.patch (1)

5-11: LGTM — removes unnecessary zero‑delay timeout.

Also applies to: 15-22

components/header-search-button.tsx (1)

76-76: LGTM — scale=2 improves image fidelity.

components/followup-panel.tsx (1)

10-29: LGTM — drawnFeatures included consistently.

components/chat-panel.tsx (2)

118-120: LGTM — drawnFeatures added to submission payload.


157-157: LGTM — dependency array now captures setSuggestions.

lib/agents/resolution-search.tsx (1)

49-51: LGTM — drawn feature geometry is now surfaced to the model.

components/search-related.tsx (1)

28-43: LGTM - Clean refactor from form submission to click handler.

The click-driven approach is simpler and removes the need for form submission mechanics. The FormData construction and submission flow is well-structured.

components/chat.tsx (2)

17-19: LGTM - Usage toggle integration.

Proper import and integration of the usage toggle context and view components.


106-123: LGTM - Good extraction of renderSuggestions helper.

This helper function reduces code duplication between mobile and desktop layouts for rendering the suggestions overlay.

app_actions_diff.patch (1)

1-54: Patch file reviewed - detailed review in full file.

This patch shows a subset of the changes to app/actions.tsx. The full file is provided separately for comprehensive review.

lib/agents/researcher.tsx (3)

15-15: LGTM - Proper type import.

The DrawnFeature type is correctly imported from the resolution-search module where it's defined.


20-27: LGTM - Well-structured conditional prompt enhancement.

The drawn features section is conditionally included in the system prompt only when features exist. The formatting clearly describes each feature's type and measurement for the AI context.


88-89: LGTM - Clean function signature extension.

The optional drawnFeatures parameter is properly added and passed through to the system prompt generation.

Also applies to: 105-105

app/actions.tsx (3)

44-50: LGTM - Robust drawnFeatures parsing.

Good defensive coding with try/catch for JSON parsing and fallback to empty array on failure.


160-163: Good: Image dataUrl preserved in resolution_search_result.

Storing the image dataUrl alongside the analysis result enables proper reconstruction of the UI state when loading from history.


753-769: LGTM - Map preview rendering for geospatial tool.

Good enhancement to render a preview image before the MapQueryHandler when a mapUrl is available from the tool response.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 116 to 124
const sanitizedMessages: CoreMessage[] = messages.map(m => {
if (Array.isArray(m.content)) {
return {
...m,
content: m.content.filter(part => part.type !== 'image')
content: m.content.filter((part: any) => part.type !== 'image')
} as CoreMessage
}
return m
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider consolidating image sanitization logic.

There are two similar sanitization patterns in this file:

  1. Lines 116-124: Filters out image parts entirely from messages
  2. Lines 126-137: Replaces image data with "IMAGE_PROCESSED" placeholder

These serve different purposes but the logic is similar. Consider extracting helper functions to clarify intent and reduce duplication.

Suggested helpers
// Remove image parts entirely (for AI context)
const stripImageParts = (messages: CoreMessage[]): CoreMessage[] =>
  messages.map(m => {
    if (Array.isArray(m.content)) {
      return { ...m, content: m.content.filter((part: any) => part.type !== 'image') } as CoreMessage;
    }
    return m;
  });

// Replace image data with placeholder (for storage/history)
const sanitizeImageData = (messages: AIMessage[]): AIMessage[] =>
  messages.map(m => {
    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;
  });
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 116 - 124, Extract the duplicated
image-sanitization logic into two small helper functions and replace the inline
code around sanitizedMessages and the subsequent image-placeholder block: create
stripImageParts(messages: CoreMessage[]) to remove parts with part.type ===
'image' and use it where sanitizedMessages is built, and create
sanitizeImageData(messages: AIMessage[]) to map user messages and replace image
fields with the "IMAGE_PROCESSED" placeholder, then call that helper in the
storage/history path; update callers to use these helpers (referencing
sanitizedMessages, stripImageParts, and sanitizeImageData) to keep intent clear
and remove duplication.

Comment on lines +126 to +137
const currentMessages = aiState.get().messages;
const sanitizedHistory = currentMessages.map(m => {
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
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: sanitizedHistory is created but not used.

The code creates sanitizedHistory (lines 126-137) to replace image data with "IMAGE_PROCESSED", but then aiState.done() uses ...aiState.get().messages (line 150) instead of sanitizedHistory. This means the sanitization is never applied to the final state.

🐛 Proposed fix
         aiState.done({
           ...aiState.get(),
           messages: [
-            ...aiState.get().messages,
+            ...sanitizedHistory,
             {
               id: groupeId,
               role: 'assistant',

Also applies to: 147-150

🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 126 - 137, The code builds sanitizedHistory but
then still passes aiState.get().messages into aiState.done, so the sanitized
image replacements are never applied; update calls that use
aiState.get().messages (notably the aiState.done(...) invocation and any nearby
usages around the end of the action) to pass sanitizedHistory instead (or
construct a finalMessages variable set to sanitizedHistory and use that) so the
sanitized content is what gets saved/emitted; ensure you only change the
argument passed to aiState.done (and any other subsequent consumers) to
reference sanitizedHistory rather than aiState.get().messages.

<div className="mobile-layout-container">
<div className="mobile-map-section">
{activeView ? <SettingsView /> : <MapProvider />}
{activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider />}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting the view rendering logic.

The nested ternary activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider /> appears twice (mobile and desktop). Consider extracting to a helper function for consistency and maintainability.

Optional: Extract view rendering logic
const renderRightPanel = () => {
  if (activeView) return <SettingsView />;
  if (isUsageOpen) return <UsageView />;
  return <MapProvider />;
};

Then use {renderRightPanel()} on lines 132 and 211.

🤖 Prompt for AI Agents
In `@components/chat.tsx` at line 132, The duplicated nested ternary rendering
logic for the right panel should be extracted into a single helper (e.g.,
renderRightPanel) to avoid repetition and ensure consistency; implement a
function that returns <SettingsView /> when activeView is truthy, <UsageView />
when isUsageOpen is truthy, otherwise <MapProvider />, and replace both
occurrences of the ternary (the one using activeView ? <SettingsView /> :
isUsageOpen ? <UsageView /> : <MapProvider />) with a single call to that helper
in the component.

Comment on lines +30 to +45
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">
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove glassmorphic classes — dependency is gone.
These classes won’t resolve after the package removal, so the new component loses intended styling. Replace with Tailwind utilities consistent with the rest of the PR.

🛠️ Proposed fix
-          <motion.div
-            className="w-fit cursor-pointer relative glassmorphic overflow-hidden rounded-lg border bg-muted"
+          <motion.div
+            className="w-fit cursor-pointer relative backdrop-blur-md bg-background/30 border border-border/40 shadow-lg overflow-hidden rounded-md"
             whileHover={{ scale: 1.02 }}
             whileTap={{ scale: 0.98 }}
           >
...
-        <DialogContent className="sm:max-w-5xl max-h-[90vh] p-1 glassmorphic border-none">
+        <DialogContent className="sm:max-w-5xl max-h-[90vh] p-1 backdrop-blur-md bg-background/80 border border-border/40 shadow-lg rounded-md">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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">
className="w-fit cursor-pointer relative backdrop-blur-md bg-background/30 border border-border/40 shadow-lg overflow-hidden rounded-md"
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 backdrop-blur-md bg-background/80 border border-border/40 shadow-lg rounded-md">
🤖 Prompt for AI Agents
In `@components/resolution-image.tsx` around lines 30 - 45, Remove the obsolete
"glassmorphic" utility classes from the component JSX (look for className on the
motion.div wrapper and DialogContent) and replace them with equivalent Tailwind
utility classes used elsewhere in the PR (e.g., backdrop-blur, bg-white/
bg-neutral with opacity, border/ ring, and shadow utilities) so the visual
styling is preserved; update className on the motion.div containing Card and on
the DialogContent component to use these Tailwind utilities while keeping
existing layout classes like rounded-lg, overflow-hidden, max-h, p-1, etc.

Comment on lines +159 to +161
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}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n lib/agents/tools/geospatial.tsx | sed -n '150,300p'

Repository: QueueLab/QCX

Length of output: 7432


🏁 Script executed:

cat -n lib/agents/tools/geospatial.tsx | sed -n '400,420p'

Repository: QueueLab/QCX

Length of output: 1499


🌐 Web query:

Google Maps Static API server-side API key security restrictions and client-side usage

💡 Result:

Server-side (recommended for Maps Static API)

  • Keep the API key private and call the Maps Static API from your backend.
  • In Google Cloud Console, set:
    • Application restriction = IP addresses (or CIDR ranges) for the server(s) that will make the requests. This is Google’s intended restriction type for server-to-server web service REST calls. [1]
    • API restriction = “Maps Static API” so the key can’t be used with other Maps APIs. [1]
  • Digitally sign Static Maps URLs using a URL signing secret (keep the secret server-side; don’t ship it to browsers). Google “strongly recommend[s]” using both an API key and a digital signature. [2]
  • Optionally limit/disable unsigned requests (quota setting) so even a stolen key can’t be used without a valid signature. [2]

Client-side usage (e.g., <img src="...maps/api/staticmap?...&key=...">)

  • Assume the API key will be visible to users (and potentially copied).
  • If you must embed it client-side, Google’s guidance is to apply:
    • Website (HTTP referrer) restriction, and
    • API restriction to the Static API. [1]
  • Do not expose the URL signing secret in client code/pages; anyone who has it can spoof signed requests. [3]

Practical guidance

  • If you need static images on a web page but want strong protection, the usual pattern is:
    1. Browser hits your backend → 2) backend generates the Static Maps request (optionally signed) using an IP-restricted key → 3) backend returns the image (or a short-lived URL you control). This aligns with Google’s server-side restriction model and avoids relying on client-side key secrecy. [1][2]

Sources: Google Maps Platform API security best practices [1], Maps Static API digital signature docs [2], Google digital signature guide (secret handling warning) [3].


Prevent leaking server-restricted Google Maps API keys in client-visible URLs.

getGoogleStaticMapUrl falls back to GOOGLE_MAPS_API_KEY (a server-side key), which is embedded in a URL returned in mcpData (lines 290, 411). This URL can be logged, cached, or sent to the client, exposing a key that should remain server-only. Google's security guidance requires server-side API keys to be restricted by IP and used only in server-to-server calls; embedding them in client-visible URLs defeats these protections.

Either:

  1. Use only NEXT_PUBLIC_GOOGLE_MAPS_API_KEY (restricted to HTTP referrer), or
  2. Move map generation to the backend and return the image/rendered map instead of a raw URL.
Proposed fix (option 1: client-only key)
-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 '';
+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}`;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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}`;
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 159 - 161,
getGoogleStaticMapUrl currently falls back to a server-only key
(GOOGLE_MAPS_API_KEY) and may embed it in client-visible URLs (used by mcpData
consumers); update getGoogleStaticMapUrl to only use
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY and never fall back to GOOGLE_MAPS_API_KEY,
returning an empty string or null when the public key is absent, and update any
call sites that expect a URL (e.g., where mcpData is populated) to handle the
missing URL gracefully; alternatively, if server-side rendering of the image is
preferred, move map generation into a backend endpoint that uses
GOOGLE_MAPS_API_KEY and returns the image or signed URL instead of exposing the
raw key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants