Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cl
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: https://www.paypal.com/ncp/payment/H7JHQ5LZPY63S
custom: https://paypal.me/queuenorth
165 changes: 48 additions & 117 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,165 +1,96 @@
<div align="center">

# QCX
**Quality Computer Experience**
Language → Maps
A General Intelligence Interface, A planet computer core
# Quality Computer Experience

<br />

[![Watch the demo](https://img.youtube.com/vi/y8M6qaFeZOw/0.jpg)](https://youtu.be/y8M6qaFeZOw?si=eYMXxqh7vQ4I-Mhr)

<br />

[**Try Live**](https://www.queue.cx) &nbsp;•&nbsp;
[**Pricing / Pre-sale**](https://www.paypal.com/ncp/payment/G82N9CZ6ZLLCQ) &nbsp;•&nbsp;
[**@tryqcx on X**](https://x.com/tryqcx) &nbsp;•&nbsp;
[**Documentation**](https://deepwiki.com/QueueLab/QCX)
[**Pricing**](https://buy.stripe.com/14A3cv7K72TR3go14Nasg02) &nbsp;|&nbsp; [**Land**](https://wwww.queue.cx) &nbsp;|&nbsp; [**X**](https://x.com/tryqcx)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the landing-page URL typo.

https://wwww.queue.cx has an extra w, so the primary “Land” link is broken.

Suggested fix
-[**Pricing**](https://buy.stripe.com/14A3cv7K72TR3go14Nasg02) &nbsp;|&nbsp; [**Land**](https://wwww.queue.cx) &nbsp;|&nbsp; [**X**](https://x.com/tryqcx)
+[**Pricing**](https://buy.stripe.com/14A3cv7K72TR3go14Nasg02) &nbsp;|&nbsp; [**Land**](https://www.queue.cx) &nbsp;|&nbsp; [**X**](https://x.com/tryqcx)
📝 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
[**Pricing**](https://buy.stripe.com/14A3cv7K72TR3go14Nasg02) &nbsp;|&nbsp; [**Land**](https://wwww.queue.cx) &nbsp;|&nbsp; [**X**](https://x.com/tryqcx)
[**Pricing**](https://buy.stripe.com/14A3cv7K72TR3go14Nasg02) &nbsp;|&nbsp; [**Land**](https://www.queue.cx) &nbsp;|&nbsp; [**X**](https://x.com/tryqcx)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 7, The README contains a typo in the [**Land**] link URL
(currently "https://wwww.queue.cx"); update the link target in the README
markdown so it reads "https://www.queue.cx" (remove the extra 'w') to restore
the landing-page link; locate the markdown line containing the
"[**Land**](https://wwww.queue.cx)" entry and replace the URL accordingly.


<a href="https://www.producthunt.com/products/qcx?embed=true&utm_source=badge-featured&utm_medium=badge&utm_source=badge-qcx" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1035588&theme=light&t=1762583679476" alt="QCX - Artificial&#0032;General&#0032;Intelligence&#0046; | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>

## What is QCX?
<img width="1277" alt="Screen Shot 2024-06-18 at 4 27 51 PM" src="https://github.com/QueueLab/MapGPT/assets/115367894/01584e12-b3f5-41c9-8009-a16642568798">

QCX is an experimental **AI-first geospatial companion** that lets you explore the planet (and beyond) through natural language + interactive maps.

- Chat with AI about any location, event, or spatial question
- Draw on the map → get measurements, analysis & AI insights
- Real-time geolocation, 3D view, time-zone aware reasoning
- Powered by multi-agent orchestration + generative UI
- Currently interpolating full chat ↔ map bidirectional integration

Built as a research prototype from **QueueLab** — exploring the frontier between natural language, spatial reasoning and artificial general intelligence.

<br />

## ✨ Features (Current & In-progress)
## Contributing

- Conversational geospatial queries with tool-using agents
- Interactive Mapbox map with drawing, measurements & GeoJSON
- Generative UI via Vercel AI SDK (streaming React components)
- Multi-model support (Grok, OpenAI, Google, Bedrock, …)
- Efficient task routing to minimize unnecessary LLM calls
- Persistent chat + map state (Redis + database)
- Mobile-responsive chat + map experience
Welcome! Please see the issues for items that need attention, and below for some tools to aid in development and debugging. We're working to interpolate chat functionality onto the map module.

<br />
Documentation.

## Tech Stack
https://deepwiki.com/QueueLab/QCX

| Category | Technology |
|------------------------|-----------------------------------------|
| Framework | [Next.js 15](https://nextjs.org/) (App Router + RSC) |
| Language | TypeScript 5.x + React 19 |
| Runtime | [Bun](https://bun.sh/) |
| AI SDK | [Vercel AI SDK](https://sdk.vercel.ai/) |
| Models | Grok, OpenAI, Google, Amazon Bedrock, … |
| Search / RAG | [Tavily](https://tavily.com/), [Exa](https://exa.ai/) |
| Database / Cache | [Upstash Redis](https://upstash.com/), PostgreSQL + Drizzle |
| UI Components | [shadcn/ui](https://ui.shadcn.com/), [Radix UI](https://www.radix-ui.com/) |
| Styling | [Tailwind CSS](https://tailwindcss.com/) + Framer Motion |
| Maps | [Mapbox GL JS](https://www.mapbox.com/) + Mapbox Draw |
| Alternative Maps | Google Maps (optional) |
### Running the app on your own machine

<br />

## Quick Start – Run Locally
## Stack

### 1. Prerequisites
- App framework: [Next.js](https://nextjs.org/)
- Text streaming / Generative UI: [Vercel AI SDK](https://sdk.vercel.ai/docs)
- Generative Model [Varies](https://openai.com/)
- Search API: [Tavily AI](https://tavily.com/) / [Exa AI](https://exa.ai/)
- Serverless Database: [Upstash](https://upstash.com/)
- Component library: [shadcn/ui](https://ui.shadcn.com/)
- Headless component primitives: [Radix UI](https://www.radix-ui.com/)
- Styling: [Tailwind CSS](https://tailwindcss.com/)
- Mapping : [Mapbox]
(https://www.mapbox.com/)

- [Bun](https://bun.sh/) ≥ 1.1
- Node.js (only for compatibility checks — app runs on Bun)

```bash
# Install Bun (if not already installed)
curl -fsSL https://bun.sh/install | bash
```

### 2. Clone & Install
### 2. Install dependencies

```bash
git clone https://github.com/QueueLab/QCX.git
cd QCX
```
install bun package manager
bun install
bun run build
bun run dev
Comment on lines +46 to +50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add languages to the fenced code blocks.

markdownlint is already flagging these blocks. Tagging them as bash / dotenv will clear the warnings and make the setup snippets easier to read.

Suggested fix
-```
+```bash
 install bun package manager
 bun install
 bun run build
 bun run dev

```diff
-```
+```bash
 cp .env.local.example .env.local

```diff
-```
+```dotenv
 # XAI API key
 XAI_API_KEY=
 ...

```diff
-```
+```bash
 bun run dev
</details>


Also applies to: 59-60, 65-77, 87-89

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>

[warning] 46-46: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @README.md around lines 46 - 50, Update the README fenced code blocks to
include language tags so markdownlint stops flagging them: add ```bash to blocks
containing shell commands (e.g., the block starting with "install bun package
manager", the blocks at 59-60 and 65-77 and the final "bun run dev"), and add

showing ".env.local.example" / "XAI_API_KEY="). Ensure each opening ``` is
followed by the appropriate language label and the closing ``` remains
unchanged.

```

### 3. Environment Variables
### 3. Setting up Upstash Redis

Follow the guide below to set up Upstash Redis. Create a database and obtain `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`. Refer to the [Upstash guide](https://upstash.com/blog/rag-chatbot-upstash#setting-up-upstash-redis) for instructions on how to proceed.

```bash
### 4. Fill out secrets

```
cp .env.local.example .env.local
```

Fill in `.env.local`:
Your .env.local file should look like this:

```env
# AI providers (at least one required)
XAI_API_KEY= # https://console.x.ai
OPENAI_API_KEY= # optional
GOOGLE_API_KEY= # optional (Gemini)
BEDROCK_ACCESS_KEY_ID= # optional
BEDROCK_SECRET_ACCESS_KEY= # optional
```
# XAI API key retrieved here: https://platform.openai.com/api-keys
XAI_API_KEY=

# Search
# Tavily API Key retrieved here: https://app.tavily.com/home
TAVILY_API_KEY=
# or
EXA_API_KEY=

# Upstash Redis (for rate limiting, caching, prompt storage)
# Upstash Redis URL and Token retrieved here: https://console.upstash.com/redis
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# Mapbox (required for maps)
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=

# Optional: database, analytics, etc.
#Mapbox access token
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN
```
Comment on lines +57 to 77
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The local env setup docs are internally inconsistent.

Following cp .env.local.example .env.local will not produce the file shown here: .env.local.example:1-27 does not include XAI_API_KEY, TAVILY_API_KEY, UPSTASH_REDIS_REST_URL, or UPSTASH_REDIS_REST_TOKEN. This snippet also points XAI_API_KEY to OpenAI’s key page on Line 66, and Line 76 is missing the = for NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN. New contributors will hit setup failures before they can run the app.

Suggested fix
 ### 4. Fill out secrets
 
-```
+```bash
 cp .env.local.example .env.local

Your .env.local file should look like this:

- -# XAI API key retrieved here: https://platform.openai.com/api-keys +dotenv
+# XAI API key
XAI_API_KEY=

Tavily API Key retrieved here: https://app.tavily.com/home

TAVILY_API_KEY=

Upstash Redis URL and Token retrieved here: https://console.upstash.com/redis

UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
-#Mapbox access token
-NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN
+
+# Mapbox access token
+NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=

If the intended flow is to bootstrap from .env.local.example, that template should be updated in the same PR as well.

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 59-59: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 65-65: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 57 - 77, The README's "Fill out secrets" section is
inconsistent with the .env.local.example template and contains a typo (missing
"=") and an incorrect XAI API link; update the README snippet and the
.env.local.example template together so the cp instruction is correct and the
example .env includes the exact variables XAI_API_KEY, TAVILY_API_KEY,
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN, and
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN (with an equals sign), remove or correct the
misleading OpenAI API link for XAI_API_KEY, and add brief comments for each
variable (e.g., # XAI API key, # Tavily API key, # Upstash Redis URL/Token, #
Mapbox access token) so new contributors can bootstrap from .env.local.example
without failing.


> **Note**: Complex generative UI and tool calling works best with frontier models (Grok family, GPT-4o, Claude 3.5/4, Gemini 1.5/2, etc.). Smaller or local models may not format output correctly.

### 4. Run

```bash
bun run dev
# or for production build preview
bun run build && bun run start
```

Open http://localhost:3000

<br />

## Contributing
_Note: This project focuses on Generative UI and requires complex output from LLMs. Currently, it's assumed that the official state of the art models will be used. Although it's possible to set up other models, if you use an Standard-compatible model, but we don't guarantee that it'll work._

We welcome contributions — especially around:
### 5. Run app locally

- Better map ↔ chat integration
- New geospatial tools / agents
- UI/UX polish (mobile especially)
- Model output parsing robustness
- Performance optimizations

1. See open [issues](https://github.com/QueueLab/QCX/issues)
2. Fork & create a branch
3. Submit a PR with clear description

Read the in-depth architecture & component docs here:
→ https://deepwiki.com/QueueLab/QCX

<br />

## Verified Models (Stable Output Formatting)

- Grok-3-mini
- (add more models as tested — PRs welcome!)

Models with reasoning / heavy tool calling can sometimes break generative UI — test carefully.

<br />

<div align="center">

**QCX** — Language is the new UI for exploring worlds.
```
bun run dev
```

Made with curiosity by [QueueLab](https://github.com/QueueLab)
You can now visit http://localhost:3000.

</div>
## Verified models

List of non reasoning verified models
Grok-3-mini
Comment on lines +83 to +96
Copy link
Copy Markdown
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:

#!/bin/bash
set -euo pipefail

echo "Searching for structured-output usage:"
rg -n -C2 '\bai\.generateObject\b|\bgenerateObject\s*\(' -g '!node_modules' -g '!dist'

echo
echo "Searching for model/provider configuration references:"
rg -n -C2 'grok|gpt-4o|XAI_API_KEY|OPENAI_API_KEY' -g '!node_modules' -g '!dist'

Repository: QueueLab/QCX

Length of output: 4189


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding all generateObject() usage across codebase ==="
rg -n 'generateObject\s*\(' --type typescript --type tsx -C1

echo
echo "=== Checking if Grok-3-mini is mentioned or tested anywhere ==="
rg -n -i 'grok.3.mini|grok-3-mini' -g '!node_modules' -g '!dist'

echo
echo "=== Verifying model configuration in tests or config files ==="
find . -type f \( -name '*.test.*' -o -name '*.spec.*' -o -name 'config.*' -o -name '.env*' \) ! -path '*/node_modules/*' ! -path '*/dist/*' 2>/dev/null | head -20

Repository: QueueLab/QCX

Length of output: 166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Finding all generateObject() usage ==="
rg -n 'generateObject\s*\(' -g '*.tsx' -g '*.ts' -g '!node_modules' -g '!dist' -C1

echo
echo "=== Checking if Grok-3-mini is mentioned anywhere in codebase ==="
rg -n -i 'grok.3.mini|grok-3-mini' -g '!node_modules' -g '!dist'

echo
echo "=== Extracting README section around verified models ==="
sed -n '80,110p' README.md

Repository: QueueLab/QCX

Length of output: 890


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Searching for workers mentioned in learnings ==="
rg -n 'geojsonParser|map-command-generator|feedback-analyzer|resolution-search' -g '!node_modules' -g '!dist'

echo
echo "=== Finding all .tsx/.ts files in lib/agents and lib/workers directories ==="
find lib -type f \( -name '*.tsx' -o -name '*.ts' \) ! -path '*/node_modules/*' 2>/dev/null | sort

echo
echo "=== Checking if lib/workers exists ==="
ls -la lib/workers 2>/dev/null || echo "lib/workers does not exist"

echo
echo "=== Listing all files in lib/agents ==="
ls -la lib/agents/ 2>/dev/null || echo "lib/agents does not exist"

Repository: QueueLab/QCX

Length of output: 2039


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking resolution-search.tsx for generateObject ==="
rg -n 'generateObject' lib/agents/resolution-search.tsx

echo
echo "=== Checking all agent files for generateObject ==="
rg -n 'generateObject' lib/agents/*.tsx

echo
echo "=== Searching for worker names in all lib files ==="
rg -n 'geojsonParser|map.command.generator|feedback.analyzer' lib/

echo
echo "=== Checking schema files for generateObject ==="
rg -n 'generateObject' lib/schema/

Repository: QueueLab/QCX

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking task-manager.tsx for generateObject schema/usage ==="
sed -n '15,40p' lib/agents/task-manager.tsx

echo
echo "=== Checking if Grok-3-mini is documented as verified anywhere in codebase ==="
rg -n -i 'grok.3' -g '!node_modules' -g '!dist'

echo
echo "=== Listing all references to model names in README ==="
grep -n -i 'grok\|gpt\|model' README.md | head -30

Repository: QueueLab/QCX

Length of output: 3062


Clarify model support in the verified models section to prevent broken local setups.

The README currently lists "Grok-3-mini" under verified models without noting that xAI's grok models cannot handle structured-output requests. Since task-manager.tsx uses ai.generateObject() and the default model selection tries grok-4-fast-non-reasoning first, users following the README will encounter failures. The documentation must explicitly state that structured-output flows require OpenAI gpt-4o.

Suggested wording
-_Note: This project focuses on Generative UI and requires complex output from LLMs. Currently, it's assumed that the official state of the art models will be used. Although it's possible to set up other models, if you use an Standard-compatible model, but we don't guarantee that it'll work._
+_Note: This project relies on structured-output LLM flows in several workers. OpenAI `gpt-4o` should be used for those paths. Other OpenAI-compatible providers may work for some chat flows, but they are not guaranteed to support the structured-output requirements used throughout the app._
 
 ## Verified models
 
-List of non reasoning verified models
-Grok-3-mini
+Verified for general chat flows only:
+- Grok-3-mini
+
+Required for structured-output workers:
+- gpt-4o
📝 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
_Note: This project focuses on Generative UI and requires complex output from LLMs. Currently, it's assumed that the official state of the art models will be used. Although it's possible to set up other models, if you use an Standard-compatible model, but we don't guarantee that it'll work._
We welcome contributions — especially around:
### 5. Run app locally
- Better map ↔ chat integration
- New geospatial tools / agents
- UI/UX polish (mobile especially)
- Model output parsing robustness
- Performance optimizations
1. See open [issues](https://github.com/QueueLab/QCX/issues)
2. Fork & create a branch
3. Submit a PR with clear description
Read the in-depth architecture & component docs here:
https://deepwiki.com/QueueLab/QCX
<br />
## Verified Models (Stable Output Formatting)
- Grok-3-mini
- (add more models as tested — PRs welcome!)
Models with reasoning / heavy tool calling can sometimes break generative UI — test carefully.
<br />
<div align="center">
**QCX** — Language is the new UI for exploring worlds.
```
bun run dev
```
Made with curiosity by [QueueLab](https://github.com/QueueLab)
You can now visit http://localhost:3000.
</div>
## Verified models
List of non reasoning verified models
Grok-3-mini
_Note: This project relies on structured-output LLM flows in several workers. OpenAI `gpt-4o` should be used for those paths. Other OpenAI-compatible providers may work for some chat flows, but they are not guaranteed to support the structured-output requirements used throughout the app._
### 5. Run app locally
🧰 Tools
🪛 LanguageTool

[grammar] ~83-~83: Use a hyphen to join words.
Context: ...ly, it's assumed that the official state of the art models will be used. Although...

(QB_NEW_EN_HYPHEN)


[grammar] ~83-~83: Use a hyphen to join words.
Context: ... it's assumed that the official state of the art models will be used. Although it...

(QB_NEW_EN_HYPHEN)


[grammar] ~83-~83: Use a hyphen to join words.
Context: ...s assumed that the official state of the art models will be used. Although it's p...

(QB_NEW_EN_HYPHEN)


[grammar] ~95-~95: Use a hyphen to join words.
Context: ...t:3000. ## Verified models List of non reasoning verified models Grok-3-mini

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.21.0)

[warning] 87-87: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 83 - 96, Update the "Verified models" section in
README to explicitly warn that xAI grok models (e.g., grok-3-mini or
grok-4-fast-non-reasoning) do not support structured-output requests used by the
app; reference that task-manager.tsx calls ai.generateObject() and that the
default model selection may try grok-4-fast-non-reasoning first, and state that
structured-output flows require an OpenAI gpt-4o model (or other verified
structured-output-compatible model) to avoid broken local setups.

10 changes: 5 additions & 5 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
border-bottom: 1px solid hsl(var(--border));
display: flex;
align-items: center;
padding: 0 10px;
padding: 0 5px;
z-index: 20;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
Expand All @@ -144,8 +144,8 @@

.mobile-icons-bar-content {
display: flex;
gap: 20px;
padding: 0 10px;
gap: 10px;
padding: 0 5px;
min-width: max-content;
/* justify-content: space-between; */
}
Expand Down Expand Up @@ -189,7 +189,7 @@

.mobile-chat-input-area {
height: auto;
padding: 10px;
padding: 5px;
background-color: hsl(var(--background));
/* border-top: 1px solid hsl(var(--border)); */ /* Removed for cleaner separation */
border-bottom: 1px solid hsl(var(--border)); /* Added for separation from messages area below */
Expand All @@ -205,7 +205,7 @@
/* left: 0; */ /* Handled by parent flex */
/* right: 0; */ /* Handled by parent flex */
width: 100%; /* Ensure it takes full width of its container */
padding: 10px;
padding: 5px;
background-color: hsl(var(--background));
/* border-top: 1px solid hsl(var(--border)); */ /* Removed to avoid double border */
/* z-index: 30; */ /* No longer needed */
Expand Down
30 changes: 26 additions & 4 deletions components/header-search-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,26 @@ export function HeaderSearchButton() {
useEffect(() => {
// Portals can only be used on the client-side after the DOM has mounted
setDesktopPortal(document.getElementById('header-search-portal'))
setMobilePortal(document.getElementById('mobile-header-search-portal'))

// Mobile portal might mount later, so check periodically
const checkMobilePortal = () => {
const el = document.getElementById('mobile-header-search-portal')
if (el) {
setMobilePortal(el)
return true
}
return false
}

if (!checkMobilePortal()) {
const interval = setInterval(() => {
if (checkMobilePortal()) {
clearInterval(interval)
}
}, 500)

return () => clearInterval(interval)
}
Comment on lines +37 to +55
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Portal lifecycle evidence ==\n'
rg -n -C3 'mobile-header-search-portal|if \(isMobile\)|<MobileIconsBar' \
  components/header-search-button.tsx \
  components/mobile-icons-bar.tsx \
  components/chat.tsx

Repository: QueueLab/QCX


Repository: QueueLab/QCX
Exit code: 0

stdout:


== Portal lifecycle evidence ==
components/chat.tsx-123-  };
components/chat.tsx-124-
components/chat.tsx-125-  // Mobile layout
components/chat.tsx:126:  if (isMobile) {
components/chat.tsx-127-    return (
components/chat.tsx-128-      <MapDataProvider> {/* Add Provider */}
components/chat.tsx-129-        <HeaderSearchButton />
--
components/chat.tsx-132-          {activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider />}
components/chat.tsx-133-        </div>
components/chat.tsx-134-        <div className="mobile-icons-bar">
components/chat.tsx:135:          <MobileIconsBar onAttachmentClick={handleAttachment} onSubmitClick={handleMobileSubmit} />
components/chat.tsx-136-        </div>
components/chat.tsx-137-        <div className="mobile-chat-input-area">
components/chat.tsx-138-          <ChatPanel 
--
components/mobile-icons-bar.tsx-27-  onSubmitClick: () => void;
components/mobile-icons-bar.tsx-28-}
components/mobile-icons-bar.tsx-29-
components/mobile-icons-bar.tsx:30:export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClick, onSubmitClick }) => {
components/mobile-icons-bar.tsx-31-  const [, setMessages] = useUIState<typeof AI>()
components/mobile-icons-bar.tsx-32-  const { clearChat } = useActions()
components/mobile-icons-bar.tsx-33-  const { toggleCalendar } = useCalendarToggle()
--
components/mobile-icons-bar.tsx-57-      <Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="mobile-calendar-button">
components/mobile-icons-bar.tsx-58-        <CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
components/mobile-icons-bar.tsx-59-      </Button>
components/mobile-icons-bar.tsx:60:      <div id="mobile-header-search-portal" className="contents" />
components/mobile-icons-bar.tsx-61-      <Button variant="ghost" size="icon" onClick={handleUsageToggle}>
components/mobile-icons-bar.tsx-62-        <TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
components/mobile-icons-bar.tsx-63-      </Button>
--
components/header-search-button.tsx-36-
components/header-search-button.tsx-37-    // Mobile portal might mount later, so check periodically
components/header-search-button.tsx-38-    const checkMobilePortal = () => {
components/header-search-button.tsx:39:      const el = document.getElementById('mobile-header-search-portal')
components/header-search-button.tsx-40-      if (el) {
components/header-search-button.tsx-41-        setMobilePortal(el)
components/header-search-button.tsx-42-        return true

Script executed:

#!/bin/bash
set -euo pipefail

# Get full context around HeaderSearchButton rendering in chat.tsx
printf '\n== HeaderSearchButton rendering context ==\n'
rg -n -B20 -A5 '<HeaderSearchButton' components/chat.tsx | head -60

# Also check if there are multiple returns/rendering paths
printf '\n== Full chat.tsx render logic ==\n'
rg -n 'return \(' components/chat.tsx

Repository: QueueLab/QCX


Repository: QueueLab/QCX
Exit code: 0

stdout:


== HeaderSearchButton rendering context ==
109-      <div className="absolute inset-0 z-20 flex flex-col items-start p-4">
110-        <SuggestionsDropdown
111-          suggestions={suggestions}
112-          onSelect={query => {
113-            setInput(query)
114-            setSuggestions(null)
115-            // Use a small timeout to ensure state update before submission
116-            setIsSubmitting(true)
117-          }}
118-          onClose={() => setSuggestions(null)}
119-          className="relative bottom-auto mb-0 w-full shadow-none border-none bg-transparent"
120-        />
121-      </div>
122-    );
123-  };
124-
125-  // Mobile layout
126-  if (isMobile) {
127-    return (
128-      <MapDataProvider> {/* Add Provider */}
129:        <HeaderSearchButton />
130-        <div className="mobile-layout-container">
131-          <div className="mobile-map-section">
132-          {activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider />}
133-        </div>
134-        <div className="mobile-icons-bar">
--
155-                      setInput(message)
156-                      setIsSubmitting(true)
157-                    }}
158-                  />
159-                ) : (
160-                  <ChatMessages messages={messages} />
161-                )}
162-              </div>
163-              {renderSuggestions()}
164-            </div>
165-          )}
166-        </div>
167-        </div>
168-      </MapDataProvider>
169-    );
170-  }
171-
172-  // Desktop layout
173-  return (
174-    <MapDataProvider> {/* Add Provider */}
175:      <HeaderSearchButton />
176-      <div className="flex justify-start items-start">
177-        {/* This is the new div for scrolling */}
178-        <div className="w-1/2 flex flex-col space-y-3 md:space-y-4 px-8 sm:px-12 pt-16 md:pt-20 pb-4 h-[calc(100vh-0.5in)] overflow-y-auto">
179-        {isCalendarOpen ? (
180-          <CalendarNotepad chatId={id} />

== Full chat.tsx render logic ==
69:    return () => window.removeEventListener('resize', checkMobile)
108:    return (
127:    return (
173:  return (

Avoid polling for a portal that only exists in the mobile branch.

HeaderSearchButton is rendered on both desktop (line 175) and mobile (line 129) in components/chat.tsx, but MobileIconsBar — which renders #mobile-header-search-portal — is only rendered inside the if (isMobile) block (line 135). On desktop sessions, this polling interval wakes every 500ms for the entire page lifetime searching for a portal that never exists. Since the portal is rendered synchronously when MobileIconsBar mounts, the polling approach is unnecessary; the component should only attempt to find the portal once after a mount or wait for a different trigger.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/header-search-button.tsx` around lines 37 - 55, HeaderSearchButton
currently polls every 500ms in checkMobilePortal looking for the
'#mobile-header-search-portal', causing unnecessary work on desktop; change the
logic so the component only looks for the portal once after mount or only when
mobile is active: remove the setInterval polling in the useEffect around
checkMobilePortal, call checkMobilePortal() once on mount (or gate the effect by
a prop/flag that indicates mobile like isMobile), and/or accept a callback/prop
from MobileIconsBar to pass the portal element (use setMobilePortal(el) when
MobileIconsBar mounts) so polling via checkMobilePortal and clearInterval are no
longer needed (references: HeaderSearchButton, checkMobilePortal,
setMobilePortal, MobileIconsBar, id 'mobile-header-search-portal').

}, [])

const handleResolutionSearch = async () => {
Expand Down Expand Up @@ -164,9 +183,12 @@ export function HeaderSearchButton() {
)

const mobileButton = (
<Button variant="ghost" size="sm" onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions}>
<Search className="h-4 w-4 mr-2" />
Search
<Button variant="ghost" size="icon" onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions} data-testid="mobile-search-button">
{isAnalyzing ? (
<div className="h-[1.2rem] w-[1.2rem] animate-spin rounded-full border-b-2 border-current"></div>
) : (
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
)}
Comment on lines +186 to +191
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't hard-disable mobile search when the provider is Google.

handleResolutionSearch() already supports the Google path via mapData.cameraState, but this button is disabled whenever map is falsy. That makes the new mobile search entry point unusable for Google-backed sessions. Since the button is now icon-only, it also needs an accessible name.

Suggested fix
+  const isSearchDisabled =
+    isAnalyzing ||
+    !actions ||
+    (mapProvider === 'mapbox' ? !map : !mapData.cameraState)
+
   const mobileButton = (
-    <Button variant="ghost" size="icon" onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions} data-testid="mobile-search-button">
+    <Button
+      variant="ghost"
+      size="icon"
+      onClick={handleResolutionSearch}
+      disabled={isSearchDisabled}
+      aria-label="Analyze current map view"
+      title="Analyze current map view"
+      data-testid="mobile-search-button"
+    >
       {isAnalyzing ? (
         <div className="h-[1.2rem] w-[1.2rem] animate-spin rounded-full border-b-2 border-current"></div>
       ) : (
         <Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
       )}
📝 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
<Button variant="ghost" size="icon" onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions} data-testid="mobile-search-button">
{isAnalyzing ? (
<div className="h-[1.2rem] w-[1.2rem] animate-spin rounded-full border-b-2 border-current"></div>
) : (
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
)}
const isSearchDisabled =
isAnalyzing ||
!actions ||
(mapProvider === 'mapbox' ? !map : !mapData.cameraState)
const mobileButton = (
<Button
variant="ghost"
size="icon"
onClick={handleResolutionSearch}
disabled={isSearchDisabled}
aria-label="Analyze current map view"
title="Analyze current map view"
data-testid="mobile-search-button"
>
{isAnalyzing ? (
<div className="h-[1.2rem] w-[1.2rem] animate-spin rounded-full border-b-2 border-current"></div>
) : (
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
)}
</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/header-search-button.tsx` around lines 167 - 172, The Button
currently disables mobile search when map is falsy, preventing Google-backed
sessions from using the new entry point; update the disabled predicate on the
Button (the element with data-testid="mobile-search-button") to allow clicks
when map is falsy but mapData.cameraState exists (since handleResolutionSearch
supports the Google path via mapData.cameraState), e.g. replace "!map ||
!actions" with a condition that requires actions and either map or
mapData.cameraState; also add an accessible name to the icon-only Button
(aria-label or aria-labelledby) so screen readers can announce the control
(reference: handleResolutionSearch, mapData.cameraState, isAnalyzing, Button,
Search).

</Button>
)

Expand Down
2 changes: 1 addition & 1 deletion components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const Header = () => {
return (
<>
<PurchaseCreditsPopup isOpen={isPurchaseOpen} onClose={() => setIsPurchaseOpen(false)} />
<header className="fixed w-full p-1 md:p-2 flex justify-between items-center z-[60] backdrop-blur bg-background/95 border-b border-border/40">
<header className="fixed w-full p-1 md:p-2 hidden md:flex justify-between items-center z-[60] backdrop-blur bg-background/95 border-b border-border/40">
<div>
<a href="/">
<span className="sr-only">Chat</span>
Expand Down
22 changes: 15 additions & 7 deletions components/mobile-icons-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { MapToggle } from './map-toggle'
import { ModeToggle } from './mode-toggle'
import { ProfileToggle } from './profile-toggle'
import { useCalendarToggle } from './calendar-toggle-context'
import { useUsageToggle } from './usage-toggle-context'
import { useProfileToggle } from './profile-toggle-context'

interface MobileIconsBarProps {
onAttachmentClick: () => void;
Expand All @@ -29,6 +31,16 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic
const [, setMessages] = useUIState<typeof AI>()
const { clearChat } = useActions()
const { toggleCalendar } = useCalendarToggle()
const { toggleUsage, isUsageOpen } = useUsageToggle()
const { activeView, closeProfileView } = useProfileToggle()

const handleUsageToggle = () => {
// If we're about to open usage and profile is open, close profile first
if (!isUsageOpen && activeView) {
closeProfileView()
}
toggleUsage()
}
Comment on lines +34 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extract the usage/profile coordination into a shared helper.

handleUsageToggle now exists here and in components/header.tsx at Lines 31-37. Duplicating that state-coupling in two components is likely to drift; a shared hook keeps mobile and desktop behavior aligned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/mobile-icons-bar.tsx` around lines 34 - 43, Duplicate logic for
coordinating usage/profile toggling (the handleUsageToggle block using
useUsageToggle and useProfileToggle) should be extracted into a shared hook so
mobile and desktop remain in sync; create a new hook (e.g.,
useUsageProfileCoordinator) that imports/useUsageToggle and useProfileToggle and
exposes a single toggle function (e.g., handleUsageToggle) which implements: if
!isUsageOpen && activeView then call closeProfileView(), then call
toggleUsage(); replace the inline handleUsageToggle in both components
(originally using useUsageToggle and useProfileToggle) with the shared hook's
returned toggle to avoid drift.


const handleNewChat = async () => {
setMessages([])
Expand All @@ -45,14 +57,10 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="mobile-calendar-button">
<CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<Button variant="ghost" size="icon" data-testid="mobile-search-button">
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
<div id="mobile-header-search-portal" className="contents" />
<Button variant="ghost" size="icon" onClick={handleUsageToggle}>
<TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<a href="https://www.paypal.com/ncp/payment/G82N9CZ6ZLLCQ" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
</a>
<Button variant="ghost" size="icon" onClick={onAttachmentClick} data-testid="mobile-attachment-button">
<Paperclip className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
Expand Down
2 changes: 1 addition & 1 deletion components/purchase-credits-popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ interface PurchaseCreditsPopupProps {

export function PurchaseCreditsPopup({ isOpen, onClose }: PurchaseCreditsPopupProps) {
const handlePurchase = () => {
window.open('https://www.paypal.com/ncp/payment/G82N9CZ6ZLLCQ', '_blank');
window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank');
onClose();
Comment on lines 21 to 23
Copy link
Copy Markdown
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:

fd components/purchase-credits-popup.tsx

Repository: QueueLab/QCX

Length of output: 38


🏁 Script executed:

cat -n components/purchase-credits-popup.tsx

Repository: QueueLab/QCX

Length of output: 2834


Keep the upgrade popup open if checkout never launches.

onClose() runs unconditionally after opening Stripe. If the browser blocks the new tab, the modal disappears even though the user never reached checkout. Only close after a successful open, and keep the popup visible otherwise.

Suggested fix
   const handlePurchase = () => {
-    window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank');
-    onClose();
+    const checkoutWindow = window.open(
+      'https://buy.stripe.com/14A3cv7K72TR3go14Nasg02',
+      '_blank',
+      'noopener,noreferrer'
+    );
+    if (checkoutWindow) {
+      onClose();
+    }
   };
📝 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://www.paypal.com/ncp/payment/G82N9CZ6ZLLCQ', '_blank');
window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank');
onClose();
const handlePurchase = () => {
const checkoutWindow = window.open(
'https://buy.stripe.com/14A3cv7K72TR3go14Nasg02',
'_blank',
'noopener,noreferrer'
);
if (checkoutWindow) {
onClose();
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/purchase-credits-popup.tsx` around lines 21 - 23, The
handlePurchase function currently calls onClose() unconditionally after
window.open; change it to only close the modal when the new window/tab was
actually opened by checking the return value of window.open (e.g., const win =
window.open(...)); call onClose() only if win is non-null (and optionally not
closed) so the popup stays open when the browser blocks opening the checkout.
Ensure you still pass the same URL and target ('_blank') when calling
window.open.

};

Expand Down
2 changes: 1 addition & 1 deletion components/usage-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function UsageView() {
<div className="p-4 border rounded-xl space-y-4">
<div className="flex justify-between items-center">
<span className="italic font-medium text-lg">Free</span>
<Button size="sm" className="rounded-full px-4" onClick={() => window.open('https://www.paypal.com/ncp/payment/G82N9CZ6ZLLCQ', '_blank')}>
<Button size="sm" className="rounded-full px-4" onClick={() => window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank')}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extract the checkout URL into shared config.

This Stripe link is now duplicated here and in components/purchase-credits-popup.tsx at Line 22. Keeping billing URLs inline in multiple components is easy to miss during future pricing or provider changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/usage-view.tsx` at line 35, The hardcoded Stripe checkout URL used
in the Button onClick
(window.open('https://buy.stripe.com/14A3cv7K72TR3go14Nasg02', '_blank')) is
duplicated in the PurchaseCreditsPopup component; extract this URL into a shared
constant (e.g., BILLING_CHECKOUT_URL) in a common config/module and import it
into both usage-view's Button onClick and the PurchaseCreditsPopup usage,
updating both to call window.open(BILLING_CHECKOUT_URL, '_blank') so future
changes are made in one place.

Upgrade
</Button>
</div>
Expand Down
Loading