Replace glassmorphic with Tailwind utilities#488
Conversation
- 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>
|
👋 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.
|
|
|
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 |
|||||||||||||||||||||||
WalkthroughRemoves 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
Sequence Diagram(s)(Skipped) Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||
There was a problem hiding this comment.
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 explicitanyin 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 intosrc, which can lead to confusing runtime behavior (e.g.,[object Object]URLs or unexpected coercions). Since the component prop is alreadyimages: string[], theanyis unnecessary and should be removed to keep type safety intact.
Summary of changes
What changed
-
Removed the
glassmorphicdependency and its CSS import:- Dropped
import 'glassmorphic/glassmorphic.css'fromcomponents/search-results-image.tsx. - Removed
"glassmorphic"frompackage.jsondependencies (lockfile updated accordingly).
- Dropped
-
Replaced
.glassmorphicusage with Tailwind utilities inSearchResultsImageSection:- Thumbnail wrapper now uses
backdrop-blur-md, semi-transparentbg-background/*,border border-border/40,shadow-lg,rounded-md, andoverflow-hidden. - The nested
Cardis made visually transparent viabg-transparent border-none shadow-noneto let the wrapper’s blur/background show through. DialogContentnow usesbackdrop-blur-md bg-background/80 border-border/40 shadow-lginstead of.glassmorphic.
- Thumbnail wrapper now uses
…(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>
There was a problem hiding this comment.
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 | 🔵 TrivialConsider extracting the Stripe URL to a shared constant.
The same Stripe checkout URL appears in multiple components (here and in
usage-view.tsxline 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 | 🔴 CriticalWrap switch cases to fix
noSwitchDeclarationserrors.The
'assistant'and'related'cases containconstdeclarations without block scoping, which violates Biome'snoSwitchDeclarationsrule. 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 | 🔵 TrivialLogo 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:
- Adding a tooltip (e.g.,
title="Open History") to indicate the action- Adding an
aria-labelfor 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
⛔ Files ignored due to path filters (1)
CC BY-NC 4.0.docxis excluded by!**/*.docx
📒 Files selected for processing (19)
app/actions.tsxapp/layout.tsxcomponents/chat-panel.tsxcomponents/chat.tsxcomponents/conditional-lottie.tsxcomponents/header-search-button.tsxcomponents/header.tsxcomponents/history-sidebar.tsxcomponents/history-toggle-context.tsxcomponents/history.tsxcomponents/map/mapbox-map.tsxcomponents/mobile-icons-bar.tsxcomponents/profile-toggle-context.tsxcomponents/profile-toggle.tsxcomponents/purchase-credits-popup.tsxcomponents/sidebar/chat-history-client.tsxcomponents/usage-toggle-context.tsxcomponents/usage-view.tsxtests/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.tsxcomponents/purchase-credits-popup.tsxcomponents/sidebar/chat-history-client.tsxcomponents/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
setSuggestionsto the dependency array is the correct fix. SincesetSuggestionsis 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
closeProfileViewmethod 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 currentai/rscstreamable 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
useHistoryTogglehook to manage sheet state, and the Suspense boundary withHistorySkeletonfallback is a good pattern for lazy-loading the chat history content.
12-12: Potential typo in import path:history-skelton→history-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
handleUsageTogglefunction 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 /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the files mentioned in the review
fd -type f -name "layout.tsx" | head -5Repository: 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
fiRepository: QueueLab/QCX
Length of output: 1053
🏁 Script executed:
# Search for HistorySidebar and related files
find . -name "*sidebar*" -o -name "*history*" | grep -i history | head -20Repository: QueueLab/QCX
Length of output: 357
🏁 Script executed:
# Search for ChatHistoryClient
find . -name "*chat*" | grep -E "(history|client)" | head -20Repository: 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 5Repository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Look for ChatHistoryClient
rg "export.*ChatHistoryClient|function ChatHistoryClient|const ChatHistoryClient" -t ts -t tsx --max-count 5Repository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Read HistorySidebar component
cat components/history-sidebar.tsxRepository: QueueLab/QCX
Length of output: 1124
🏁 Script executed:
# Read ChatHistoryClient component
cat components/sidebar/chat-history-client.tsxRepository: QueueLab/QCX
Length of output: 7606
🏁 Script executed:
# Find the /api/chats endpoint
find . -path "*/api/chats*" -type fRepository: 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 3Repository: 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 -10Repository: 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.
| useEffect(() => { | ||
| // Open payment popup as soon as application opens | ||
| setIsPurchaseOpen(true) | ||
| }, []) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for PurchaseCreditsPopup component file
fd -t f 'purchase.*popup' --ignore-caseRepository: 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 -20Repository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Read the PurchaseCreditsPopup component to check for auth/cooldown handling
cat -n components/purchase-credits-popup.tsxRepository: QueueLab/QCX
Length of output: 2832
🏁 Script executed:
# Read the full header.tsx to see auth context and state management
cat -n components/header.tsxRepository: 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 -20Repository: QueueLab/QCX
Length of output: 445
🏁 Script executed:
# Search for useAuth or auth context imports
rg -n 'useAuth|useSession|AuthContext|authContext' | head -30Repository: 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:
- Authentication guard (check if user is authenticated)
- 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.
| <Button variant="ghost" size="icon" onClick={handleUsageToggle}> | ||
| <TentTree className="h-[1.2rem] w-[1.2rem]" /> | ||
| </Button> |
There was a problem hiding this comment.
🧹 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.
| const handlePurchase = () => { | ||
| window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank'); | ||
| onClose(); |
There was a problem hiding this comment.
🧩 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
fiRepository: 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 1Repository: QueueLab/QCX
Length of output: 83
🏁 Script executed:
# Search for window.open patterns in the codebase
rg "window\.open" -A 1 --max-count 20Repository: 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.tsxRepository: 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 -5Repository: 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.
| 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.
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧹 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' | |||
There was a problem hiding this comment.
🧹 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.
| 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.
| 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) |
There was a problem hiding this comment.
🧹 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.
| 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.
| <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> | ||
| ))} |
There was a problem hiding this comment.
🧹 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.
| <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.
| // 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(); |
There was a problem hiding this comment.
🧹 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.
| // 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>
There was a problem hiding this comment.
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 | 🟡 MinorMinor:
onClickhandler could receive empty string despite filter.The filter on line 48 checks
item?.query !== '', but theonClickon line 55 usesitem?.query || ''which would pass an empty string ifqueryisundefined. 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 | 🟡 MinorWrap 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
answervariable is currently declared before the switch on thetype, but it's only used in the 'response' case. Move the declaration inside the case block.
702-714:⚠️ Potential issue | 🟡 MinorWrap switch case declarations in blocks.
The
relatedQueriesdeclaration 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
📒 Files selected for processing (13)
app/actions.tsxapp_actions_diff.patchcomponents/chat-panel.tsxcomponents/chat.tsxcomponents/followup-panel.tsxcomponents/header-search-button.tsxcomponents/resolution-image.tsxcomponents/search-related.tsxcomponents_chat_patch.patchlib/agents/researcher.tsxlib/agents/resolution-search.tsxlib/agents/tools/geospatial.tsxlib/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
DrawnFeaturetype 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
drawnFeaturesparameter 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.
| 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 | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider consolidating image sanitization logic.
There are two similar sanitization patterns in this file:
- Lines 116-124: Filters out image parts entirely from messages
- 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.
| 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 | ||
| }); |
There was a problem hiding this comment.
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 />} |
There was a problem hiding this comment.
🧹 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.
| 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"> |
There was a problem hiding this comment.
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.
| 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.
| 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:
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:
- 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:
- Use only
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY(restricted to HTTP referrer), or - 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.
| 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.
User description
This change replaces the external
glassmorphicdependency 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:
components/search-results-image.tsx, theglassmorphicclass on the image thumbnails and theDialogContentwas replaced with a combination ofbackdrop-blur-md,bg-background/30(orbg-background/80), andborder-border/40.Cardcomponent within the thumbnails was made transparent to allow the backdrop blur from the wrapper to be visible.glassmorphicpackage was removed frompackage.jsonand the import was deleted.bun installwas run to update the lockfile.PR created automatically by Jules for task 9822029702985678452 started by @ngoiyaeric
PR Type
Enhancement
Description
Removed
glassmorphicnpm package dependencyReplaced
.glassmorphicclass with Tailwind utilitiesApplied
backdrop-blur-md, theme-aware backgrounds, bordersMade nested Card components transparent for visual consistency
Diagram Walkthrough
File Walkthrough
search-results-image.tsx
Replace glassmorphic with Tailwind utilitiescomponents/search-results-image.tsx
glassmorphicCSS import statement.glassmorphicclass on image thumbnail wrapper withbackdrop-blur-md bg-background/30 border border-border/40 shadow-lgrounded-md overflow-hiddenCardcomponent transparent withbg-transparent border-noneshadow-noneclasses.glassmorphicclass onDialogContentwithbackdrop-blur-mdbg-background/80 border-border/40 shadow-lgpackage.json
Remove glassmorphic package dependencypackage.json
glassmorphicnpm package dependency (version ^0.0.3)Summary by CodeRabbit
New Features
Style
Behavior